1 #region Copyright & License Information 2 /* 3 * Copyright 2007-2020 The OpenRA Developers (see AUTHORS) 4 * This file is part of OpenRA, which is free software. It is made 5 * available to you under the terms of the GNU General Public License 6 * as published by the Free Software Foundation, either version 3 of 7 * the License, or (at your option) any later version. For more 8 * information, see COPYING. 9 */ 10 #endregion 11 12 using System; 13 using System.Linq; 14 using OpenRA.Graphics; 15 using OpenRA.Primitives; 16 using OpenRA.Widgets; 17 18 namespace OpenRA.Mods.Common.Widgets 19 { 20 public interface ILayout 21 { AdjustChild(Widget w)22 void AdjustChild(Widget w); AdjustChildren()23 void AdjustChildren(); 24 } 25 26 public enum ScrollPanelAlign 27 { 28 Bottom, 29 Top 30 } 31 32 public enum ScrollBar 33 { 34 Left, 35 Right, 36 Hidden 37 } 38 39 public class ScrollPanelWidget : Widget 40 { 41 readonly Ruleset modRules; 42 public int ScrollbarWidth = 24; 43 public int BorderWidth = 1; 44 public int TopBottomSpacing = 2; 45 public int ItemSpacing = 0; 46 public int ButtonDepth = ChromeMetrics.Get<int>("ButtonDepth"); 47 public string ClickSound = ChromeMetrics.Get<string>("ClickSound"); 48 public string Background = "scrollpanel-bg"; 49 public string ScrollBarBackground = "scrollpanel-bg"; 50 public string Button = "scrollpanel-button"; 51 public int ContentHeight; 52 public ILayout Layout; 53 public int MinimumThumbSize = 10; 54 public ScrollPanelAlign Align = ScrollPanelAlign.Top; 55 public ScrollBar ScrollBar = ScrollBar.Right; 56 public bool CollapseHiddenChildren; 57 58 // Fraction of the remaining scroll-delta to move in 40ms 59 public float SmoothScrollSpeed = 0.333f; 60 61 protected bool upPressed; 62 protected bool downPressed; 63 protected bool upDisabled; 64 protected bool downDisabled; 65 protected bool thumbPressed; 66 protected Rectangle upButtonRect; 67 protected Rectangle downButtonRect; 68 protected Rectangle backgroundRect; 69 protected Rectangle scrollbarRect; 70 protected Rectangle thumbRect; 71 72 // The target value is the list offset we're trying to reach 73 float targetListOffset; 74 75 // The current value is the actual list offset at the moment 76 float currentListOffset; 77 78 // The Game.Runtime value when UpdateSmoothScrolling was last called 79 // Used for calculating the per-frame smooth-scrolling delta 80 long lastSmoothScrollTime = 0; 81 82 // Setting "smooth" to true will only update the target list offset. 83 // Setting "smooth" to false will also set the current list offset, 84 // i.e. it will scroll immediately. 85 // 86 // For example, scrolling with the mouse wheel will use smooth 87 // scrolling to give a nice visual effect that makes it easier 88 // for the user to follow. Dragging the scrollbar's thumb, however, 89 // will scroll to the desired position immediately. SetListOffset(float value, bool smooth)90 protected void SetListOffset(float value, bool smooth) 91 { 92 targetListOffset = value; 93 if (!smooth) 94 { 95 var oldListOffset = currentListOffset; 96 currentListOffset = value; 97 98 // Update mouseover 99 if (oldListOffset != currentListOffset) 100 Ui.ResetTooltips(); 101 } 102 } 103 104 [ObjectCreator.UseCtor] ScrollPanelWidget(ModData modData)105 public ScrollPanelWidget(ModData modData) 106 { 107 modRules = modData.DefaultRules; 108 109 Layout = new ListLayout(this); 110 } 111 RemoveChildren()112 public override void RemoveChildren() 113 { 114 ContentHeight = 0; 115 base.RemoveChildren(); 116 } 117 AddChild(Widget child)118 public override void AddChild(Widget child) 119 { 120 // Initial setup of margins/height 121 Layout.AdjustChild(child); 122 base.AddChild(child); 123 } 124 RemoveChild(Widget child)125 public override void RemoveChild(Widget child) 126 { 127 base.RemoveChild(child); 128 Layout.AdjustChildren(); 129 Scroll(0); 130 } 131 ReplaceChild(Widget oldChild, Widget newChild)132 public void ReplaceChild(Widget oldChild, Widget newChild) 133 { 134 oldChild.Removed(); 135 newChild.Parent = this; 136 Children[Children.IndexOf(oldChild)] = newChild; 137 Layout.AdjustChildren(); 138 Scroll(0); 139 } 140 DrawOuter()141 public override void DrawOuter() 142 { 143 if (!IsVisible()) 144 return; 145 146 UpdateSmoothScrolling(); 147 148 var rb = RenderBounds; 149 var scrollbarHeight = rb.Height - 2 * ScrollbarWidth; 150 151 // Scroll thumb is only visible if the content does not fit within the panel bounds 152 var thumbHeight = 0; 153 var thumbOrigin = rb.Y + ScrollbarWidth; 154 if (ContentHeight > rb.Height) 155 { 156 thumbHeight = Math.Max(MinimumThumbSize, scrollbarHeight * rb.Height / ContentHeight); 157 thumbOrigin += (int)((scrollbarHeight - thumbHeight) * currentListOffset / (rb.Height - ContentHeight)); 158 } 159 160 switch (ScrollBar) 161 { 162 case ScrollBar.Left: 163 backgroundRect = new Rectangle(rb.X + ScrollbarWidth, rb.Y, rb.Width + 1, rb.Height); 164 upButtonRect = new Rectangle(rb.X, rb.Y, ScrollbarWidth, ScrollbarWidth); 165 downButtonRect = new Rectangle(rb.X, rb.Bottom - ScrollbarWidth, ScrollbarWidth, ScrollbarWidth); 166 scrollbarRect = new Rectangle(rb.X, rb.Y + ScrollbarWidth - 1, ScrollbarWidth, scrollbarHeight + 2); 167 thumbRect = new Rectangle(rb.X, thumbOrigin, ScrollbarWidth, thumbHeight); 168 break; 169 case ScrollBar.Right: 170 backgroundRect = new Rectangle(rb.X, rb.Y, rb.Width - ScrollbarWidth + 1, rb.Height); 171 upButtonRect = new Rectangle(rb.Right - ScrollbarWidth, rb.Y, ScrollbarWidth, ScrollbarWidth); 172 downButtonRect = new Rectangle(rb.Right - ScrollbarWidth, rb.Bottom - ScrollbarWidth, ScrollbarWidth, ScrollbarWidth); 173 scrollbarRect = new Rectangle(rb.Right - ScrollbarWidth, rb.Y + ScrollbarWidth - 1, ScrollbarWidth, scrollbarHeight + 2); 174 thumbRect = new Rectangle(rb.Right - ScrollbarWidth, thumbOrigin, ScrollbarWidth, thumbHeight); 175 break; 176 case ScrollBar.Hidden: 177 backgroundRect = new Rectangle(rb.X, rb.Y, rb.Width + 1, rb.Height); 178 break; 179 default: 180 throw new ArgumentOutOfRangeException(); 181 } 182 183 WidgetUtils.DrawPanel(Background, backgroundRect); 184 185 if (ScrollBar != ScrollBar.Hidden) 186 { 187 var upHover = Ui.MouseOverWidget == this && upButtonRect.Contains(Viewport.LastMousePos); 188 upDisabled = thumbHeight == 0 || currentListOffset >= 0; 189 190 var downHover = Ui.MouseOverWidget == this && downButtonRect.Contains(Viewport.LastMousePos); 191 downDisabled = thumbHeight == 0 || currentListOffset <= Bounds.Height - ContentHeight; 192 193 var thumbHover = Ui.MouseOverWidget == this && thumbRect.Contains(Viewport.LastMousePos); 194 WidgetUtils.DrawPanel(ScrollBarBackground, scrollbarRect); 195 ButtonWidget.DrawBackground(Button, upButtonRect, upDisabled, upPressed, upHover, false); 196 ButtonWidget.DrawBackground(Button, downButtonRect, downDisabled, downPressed, downHover, false); 197 198 if (thumbHeight > 0) 199 ButtonWidget.DrawBackground(Button, thumbRect, false, HasMouseFocus && thumbHover, thumbHover, false); 200 201 var upOffset = !upPressed || upDisabled ? 4 : 4 + ButtonDepth; 202 var downOffset = !downPressed || downDisabled ? 4 : 4 + ButtonDepth; 203 204 WidgetUtils.DrawRGBA(ChromeProvider.GetImage("scrollbar", upPressed || upDisabled ? "up_pressed" : "up_arrow"), 205 new float2(upButtonRect.Left + upOffset, upButtonRect.Top + upOffset)); 206 WidgetUtils.DrawRGBA(ChromeProvider.GetImage("scrollbar", downPressed || downDisabled ? "down_pressed" : "down_arrow"), 207 new float2(downButtonRect.Left + downOffset, downButtonRect.Top + downOffset)); 208 } 209 210 var drawBounds = backgroundRect.InflateBy(-BorderWidth, -BorderWidth, -BorderWidth, -BorderWidth); 211 Game.Renderer.EnableScissor(drawBounds); 212 213 // ChildOrigin enumerates the widget tree, so only evaluate it once 214 var co = ChildOrigin; 215 drawBounds.X -= co.X; 216 drawBounds.Y -= co.Y; 217 218 foreach (var child in Children) 219 if (child.Bounds.IntersectsWith(drawBounds)) 220 child.DrawOuter(); 221 222 Game.Renderer.DisableScissor(); 223 } 224 225 public override int2 ChildOrigin 226 { 227 get 228 { 229 return RenderOrigin + new int2(ScrollBar == ScrollBar.Left ? ScrollbarWidth : 0, (int)currentListOffset); 230 } 231 } 232 GetEventBounds()233 public override Rectangle GetEventBounds() 234 { 235 return EventBounds; 236 } 237 Scroll(int amount, bool smooth = false)238 void Scroll(int amount, bool smooth = false) 239 { 240 var newTarget = targetListOffset + amount * Game.Settings.Game.UIScrollSpeed; 241 newTarget = Math.Min(0, Math.Max(Bounds.Height - ContentHeight, newTarget)); 242 243 SetListOffset(newTarget, smooth); 244 } 245 ScrollToBottom(bool smooth = false)246 public void ScrollToBottom(bool smooth = false) 247 { 248 var value = Align == ScrollPanelAlign.Top ? 249 Math.Min(0, Bounds.Height - ContentHeight) : 250 Bounds.Height - ContentHeight; 251 252 SetListOffset(value, smooth); 253 } 254 ScrollToTop(bool smooth = false)255 public void ScrollToTop(bool smooth = false) 256 { 257 var value = Align == ScrollPanelAlign.Top ? 0 : 258 Math.Max(0, Bounds.Height - ContentHeight); 259 260 SetListOffset(value, smooth); 261 } 262 263 public bool ScrolledToBottom 264 { 265 get { return targetListOffset == Math.Min(0, Bounds.Height - ContentHeight) || ContentHeight <= Bounds.Height; } 266 } 267 ScrollToItem(Widget item, bool smooth = false)268 void ScrollToItem(Widget item, bool smooth = false) 269 { 270 // Scroll the item to be visible 271 float? newOffset = null; 272 if (item.Bounds.Top + currentListOffset < 0) 273 newOffset = ItemSpacing - item.Bounds.Top; 274 275 if (item.Bounds.Bottom + currentListOffset > RenderBounds.Height) 276 newOffset = RenderBounds.Height - item.Bounds.Bottom - ItemSpacing; 277 278 if (newOffset.HasValue) 279 SetListOffset(newOffset.Value, smooth); 280 } 281 ScrollToItem(string itemKey, bool smooth = false)282 public void ScrollToItem(string itemKey, bool smooth = false) 283 { 284 var item = Children.FirstOrDefault(c => 285 { 286 var si = c as ScrollItemWidget; 287 return si != null && si.ItemKey == itemKey; 288 }); 289 290 if (item != null) 291 ScrollToItem(item, smooth); 292 } 293 ScrollToSelectedItem()294 public void ScrollToSelectedItem() 295 { 296 var item = Children.FirstOrDefault(c => 297 { 298 var si = c as ScrollItemWidget; 299 return si != null && si.IsSelected(); 300 }); 301 302 if (item != null) 303 ScrollToItem(item); 304 } 305 UpdateSmoothScrolling()306 void UpdateSmoothScrolling() 307 { 308 if (lastSmoothScrollTime == 0) 309 { 310 lastSmoothScrollTime = Game.RunTime; 311 return; 312 } 313 314 var offsetDiff = targetListOffset - currentListOffset; 315 var absOffsetDiff = Math.Abs(offsetDiff); 316 if (absOffsetDiff > 1f) 317 { 318 var dt = Game.RunTime - lastSmoothScrollTime; 319 currentListOffset += offsetDiff * SmoothScrollSpeed.Clamp(0.1f, 1.0f) * dt / 40; 320 321 Ui.ResetTooltips(); 322 } 323 else 324 SetListOffset(targetListOffset, false); 325 326 lastSmoothScrollTime = Game.RunTime; 327 } 328 Tick()329 public override void Tick() 330 { 331 if (upPressed) 332 Scroll(1); 333 334 if (downPressed) 335 Scroll(-1); 336 } 337 YieldMouseFocus(MouseInput mi)338 public override bool YieldMouseFocus(MouseInput mi) 339 { 340 upPressed = downPressed = thumbPressed = false; 341 return base.YieldMouseFocus(mi); 342 } 343 344 int2 lastMouseLocation; 345 HandleMouseInput(MouseInput mi)346 public override bool HandleMouseInput(MouseInput mi) 347 { 348 if (mi.Event == MouseInputEvent.Scroll) 349 { 350 Scroll(mi.Delta.Y, true); 351 return true; 352 } 353 354 if (mi.Button != MouseButton.Left) 355 return false; 356 357 if (mi.Event == MouseInputEvent.Down && !TakeMouseFocus(mi)) 358 return false; 359 360 if (!HasMouseFocus) 361 return false; 362 363 if (HasMouseFocus && mi.Event == MouseInputEvent.Up) 364 return YieldMouseFocus(mi); 365 366 if (thumbPressed && mi.Event == MouseInputEvent.Move) 367 { 368 var rb = RenderBounds; 369 var scrollbarHeight = rb.Height - 2 * ScrollbarWidth; 370 var thumbHeight = ContentHeight == 0 ? 0 : Math.Max(MinimumThumbSize, (int)(scrollbarHeight * Math.Min(rb.Height * 1f / ContentHeight, 1f))); 371 var oldOffset = currentListOffset; 372 373 var newOffset = currentListOffset + ((int)((lastMouseLocation.Y - mi.Location.Y) * (ContentHeight - rb.Height) * 1f / (scrollbarHeight - thumbHeight))); 374 newOffset = Math.Min(0, Math.Max(rb.Height - ContentHeight, newOffset)); 375 SetListOffset(newOffset, false); 376 377 if (oldOffset != newOffset) 378 lastMouseLocation = mi.Location; 379 } 380 else 381 { 382 upPressed = upButtonRect.Contains(mi.Location); 383 downPressed = downButtonRect.Contains(mi.Location); 384 thumbPressed = thumbRect.Contains(mi.Location); 385 if (thumbPressed) 386 lastMouseLocation = mi.Location; 387 388 if (mi.Event == MouseInputEvent.Down && ((upPressed && !upDisabled) || (downPressed && !downDisabled) || thumbPressed)) 389 Game.Sound.PlayNotification(modRules, null, "Sounds", ClickSound, null); 390 } 391 392 return upPressed || downPressed || thumbPressed; 393 } 394 395 IObservableCollection collection; 396 Func<object, Widget> makeWidget; 397 Func<Widget, object, bool> widgetItemEquals; 398 bool autoScroll; 399 Unbind()400 public void Unbind() 401 { 402 Bind(null, null, null, false); 403 } 404 Bind(IObservableCollection c, Func<object, Widget> makeWidget, Func<Widget, object, bool> widgetItemEquals, bool autoScroll)405 public void Bind(IObservableCollection c, Func<object, Widget> makeWidget, Func<Widget, object, bool> widgetItemEquals, bool autoScroll) 406 { 407 this.autoScroll = autoScroll; 408 409 Game.RunAfterTick(() => 410 { 411 if (collection != null) 412 { 413 collection.OnAdd -= BindingAdd; 414 collection.OnRemove -= BindingRemove; 415 collection.OnRemoveAt -= BindingRemoveAt; 416 collection.OnSet -= BindingSet; 417 collection.OnRefresh -= BindingRefresh; 418 } 419 420 this.makeWidget = makeWidget; 421 this.widgetItemEquals = widgetItemEquals; 422 423 RemoveChildren(); 424 collection = c; 425 426 if (c != null) 427 { 428 foreach (var item in c.ObservedItems) 429 BindingAddImpl(item); 430 431 c.OnAdd += BindingAdd; 432 c.OnRemove += BindingRemove; 433 c.OnRemoveAt += BindingRemoveAt; 434 c.OnSet += BindingSet; 435 c.OnRefresh += BindingRefresh; 436 } 437 }); 438 } 439 BindingAdd(IObservableCollection col, object item)440 void BindingAdd(IObservableCollection col, object item) 441 { 442 Game.RunAfterTick(() => 443 { 444 if (collection != col) 445 return; 446 447 BindingAddImpl(item); 448 }); 449 } 450 BindingAddImpl(object item)451 void BindingAddImpl(object item) 452 { 453 if (makeWidget == null) 454 return; 455 456 var widget = makeWidget(item); 457 var scrollToBottom = autoScroll && ScrolledToBottom; 458 459 AddChild(widget); 460 461 if (scrollToBottom) 462 ScrollToBottom(); 463 } 464 BindingRemove(IObservableCollection col, object item)465 void BindingRemove(IObservableCollection col, object item) 466 { 467 Game.RunAfterTick(() => 468 { 469 if (collection != col) 470 return; 471 472 var widget = Children.FirstOrDefault(w => widgetItemEquals(w, item)); 473 if (widget != null) 474 RemoveChild(widget); 475 }); 476 } 477 BindingRemoveAt(IObservableCollection col, int index)478 void BindingRemoveAt(IObservableCollection col, int index) 479 { 480 Game.RunAfterTick(() => 481 { 482 if (collection != col) 483 return; 484 485 if (index < 0 || index >= Children.Count) 486 return; 487 488 RemoveChild(Children[index]); 489 }); 490 } 491 BindingSet(IObservableCollection col, object oldItem, object newItem)492 void BindingSet(IObservableCollection col, object oldItem, object newItem) 493 { 494 Game.RunAfterTick(() => 495 { 496 if (collection != col) 497 return; 498 499 var newWidget = makeWidget(newItem); 500 newWidget.Parent = this; 501 502 var i = Children.FindIndex(w => widgetItemEquals(w, oldItem)); 503 if (i >= 0) 504 { 505 var oldWidget = Children[i]; 506 oldWidget.Removed(); 507 Children[i] = newWidget; 508 Layout.AdjustChildren(); 509 } 510 else 511 AddChild(newWidget); 512 }); 513 } 514 BindingRefresh(IObservableCollection col)515 void BindingRefresh(IObservableCollection col) 516 { 517 Game.RunAfterTick(() => 518 { 519 if (collection != col) 520 return; 521 522 RemoveChildren(); 523 foreach (var item in collection.ObservedItems) 524 BindingAddImpl(item); 525 }); 526 } 527 } 528 } 529