A game framework written with osu! in mind.
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2// See the LICENCE file in the repository root for full licence text.
3
4using System;
5using System.Diagnostics;
6using osu.Framework.Caching;
7using osu.Framework.Input;
8using osu.Framework.Input.Bindings;
9using osu.Framework.Input.Events;
10using osu.Framework.Utils;
11using osuTK;
12using osuTK.Input;
13
14namespace osu.Framework.Graphics.Containers
15{
16 public abstract class ScrollContainer<T> : Container<T>, DelayedLoadWrapper.IOnScreenOptimisingContainer, IKeyBindingHandler<PlatformAction>
17 where T : Drawable
18 {
19 /// <summary>
20 /// Determines whether the scroll dragger appears on the left side. If not, then it always appears on the right side.
21 /// </summary>
22 public Anchor ScrollbarAnchor
23 {
24 get => Scrollbar.Anchor;
25 set
26 {
27 Scrollbar.Anchor = value;
28 Scrollbar.Origin = value;
29 updatePadding();
30 }
31 }
32
33 private bool scrollbarVisible = true;
34
35 /// <summary>
36 /// Whether the scrollbar is visible.
37 /// </summary>
38 public bool ScrollbarVisible
39 {
40 get => scrollbarVisible;
41 set
42 {
43 scrollbarVisible = value;
44 scrollbarCache.Invalidate();
45 }
46 }
47
48 protected readonly ScrollbarContainer Scrollbar;
49
50 private bool scrollbarOverlapsContent = true;
51
52 /// <summary>
53 /// Whether the scrollbar overlaps the content or resides in its own padded space.
54 /// </summary>
55 public bool ScrollbarOverlapsContent
56 {
57 get => scrollbarOverlapsContent;
58 set
59 {
60 scrollbarOverlapsContent = value;
61 updatePadding();
62 }
63 }
64
65 /// <summary>
66 /// Size of available content (i.e. everything that can be scrolled to) in the scroll direction.
67 /// </summary>
68 public float AvailableContent => ScrollContent.DrawSize[ScrollDim];
69
70 /// <summary>
71 /// Size of the viewport in the scroll direction.
72 /// </summary>
73 public float DisplayableContent => ChildSize[ScrollDim];
74
75 /// <summary>
76 /// Controls the distance scrolled per unit of mouse scroll.
77 /// </summary>
78 public float ScrollDistance = 80;
79
80 /// <summary>
81 /// This limits how far out of clamping bounds we allow the target position to be at most.
82 /// Effectively, larger values result in bouncier behavior as the scroll boundaries are approached
83 /// with high velocity.
84 /// </summary>
85 public float ClampExtension = 500;
86
87 /// <summary>
88 /// This corresponds to the clamping force. A larger value means more aggressive clamping. Default is 0.012.
89 /// </summary>
90 private const double distance_decay_clamping = 0.012;
91
92 /// <summary>
93 /// Controls the rate with which the target position is approached after ending a drag. Default is 0.0035.
94 /// </summary>
95 public double DistanceDecayDrag = 0.0035;
96
97 /// <summary>
98 /// Controls the rate with which the target position is approached after scrolling. Default is 0.01
99 /// </summary>
100 public double DistanceDecayScroll = 0.01;
101
102 /// <summary>
103 /// Controls the rate with which the target position is approached after jumping to a specific location. Default is 0.01.
104 /// </summary>
105 public double DistanceDecayJump = 0.01;
106
107 /// <summary>
108 /// Controls the rate with which the target position is approached. It is automatically set after
109 /// dragging or scrolling.
110 /// </summary>
111 private double distanceDecay;
112
113 /// <summary>
114 /// The current scroll position.
115 /// </summary>
116 public float Current { get; private set; }
117
118 /// <summary>
119 /// The target scroll position which is exponentially approached by current via a rate of distanceDecay.
120 /// </summary>
121 protected float Target { get; private set; }
122
123 /// <summary>
124 /// The maximum distance that can be scrolled in the scroll direction.
125 /// </summary>
126 public float ScrollableExtent => Math.Max(AvailableContent - DisplayableContent, 0);
127
128 /// <summary>
129 /// The maximum distance that the scrollbar can move in the scroll direction.
130 /// </summary>
131 public float ScrollbarMovementExtent => Math.Max(DrawSize[ScrollDim] - Scrollbar.DrawSize[ScrollDim], 0);
132
133 /// <summary>
134 /// Clamp a value to the available scroll range.
135 /// </summary>
136 /// <param name="position">The value to clamp.</param>
137 /// <param name="extension">An extension value beyond the normal extent.</param>
138 protected float Clamp(float position, float extension = 0) => Math.Max(Math.Min(position, ScrollableExtent + extension), -extension);
139
140 protected override Container<T> Content => ScrollContent;
141
142 /// <summary>
143 /// Whether we are currently scrolled as far as possible into the scroll direction.
144 /// </summary>
145 /// <param name="lenience">How close to the extent we need to be.</param>
146 public bool IsScrolledToEnd(float lenience = Precision.FLOAT_EPSILON) => Precision.AlmostBigger(Target, ScrollableExtent, lenience);
147
148 /// <summary>
149 /// The container holding all children which are getting scrolled around.
150 /// </summary>
151 public Container<T> ScrollContent { get; }
152
153 protected virtual bool IsDragging { get; private set; }
154
155 public bool IsHandlingKeyboardScrolling
156 {
157 get
158 {
159 if (IsHovered)
160 return true;
161
162 InputManager inputManager = GetContainingInputManager();
163 return inputManager != null && ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position);
164 }
165 }
166
167 /// <summary>
168 /// The direction in which scrolling is supported.
169 /// </summary>
170 protected readonly Direction ScrollDirection;
171
172 /// <summary>
173 /// The direction in which scrolling is supported, converted to an int for array index lookups.
174 /// </summary>
175 protected int ScrollDim => ScrollDirection == Direction.Horizontal ? 0 : 1;
176
177 /// <summary>
178 /// Creates a scroll container.
179 /// </summary>
180 /// <param name="scrollDirection">The direction in which should be scrolled. Can be vertical or horizontal. Default is vertical.</param>
181 protected ScrollContainer(Direction scrollDirection = Direction.Vertical)
182 {
183 ScrollDirection = scrollDirection;
184
185 Masking = true;
186
187 Axes scrollAxis = scrollDirection == Direction.Horizontal ? Axes.X : Axes.Y;
188 AddRangeInternal(new Drawable[]
189 {
190 ScrollContent = new Container<T>
191 {
192 RelativeSizeAxes = Axes.Both & ~scrollAxis,
193 AutoSizeAxes = scrollAxis,
194 },
195 Scrollbar = CreateScrollbar(scrollDirection)
196 });
197
198 Scrollbar.Hide();
199 Scrollbar.Dragged = onScrollbarMovement;
200 ScrollbarAnchor = scrollDirection == Direction.Vertical ? Anchor.TopRight : Anchor.BottomLeft;
201 }
202
203 private float lastUpdateDisplayableContent = -1;
204 private float lastAvailableContent = -1;
205
206 private void updateSize()
207 {
208 // ensure we only update scrollbar when something has changed, to avoid transform helpers resetting their transform every frame.
209 // also avoids creating many needless Transforms every update frame.
210 if (lastAvailableContent != AvailableContent || lastUpdateDisplayableContent != DisplayableContent)
211 {
212 lastAvailableContent = AvailableContent;
213 lastUpdateDisplayableContent = DisplayableContent;
214 scrollbarCache.Invalidate();
215 }
216 }
217
218 private readonly Cached scrollbarCache = new Cached();
219
220 private void updatePadding()
221 {
222 if (scrollbarOverlapsContent || AvailableContent <= DisplayableContent)
223 ScrollContent.Padding = new MarginPadding();
224 else
225 {
226 if (ScrollDirection == Direction.Vertical)
227 {
228 ScrollContent.Padding = ScrollbarAnchor == Anchor.TopLeft
229 ? new MarginPadding { Left = Scrollbar.Width + Scrollbar.Margin.Left }
230 : new MarginPadding { Right = Scrollbar.Width + Scrollbar.Margin.Right };
231 }
232 else
233 {
234 ScrollContent.Padding = ScrollbarAnchor == Anchor.TopLeft
235 ? new MarginPadding { Top = Scrollbar.Height + Scrollbar.Margin.Top }
236 : new MarginPadding { Bottom = Scrollbar.Height + Scrollbar.Margin.Bottom };
237 }
238 }
239 }
240
241 protected override bool OnDragStart(DragStartEvent e)
242 {
243 if (IsDragging || e.Button != MouseButton.Left || Content.AliveInternalChildren.Count == 0)
244 return false;
245
246 lastDragTime = Time.Current;
247 averageDragDelta = averageDragTime = 0;
248
249 IsDragging = true;
250
251 dragButtonManager = GetContainingInputManager().GetButtonEventManagerFor(e.Button);
252
253 return true;
254 }
255
256 protected override bool OnKeyDown(KeyDownEvent e)
257 {
258 if (IsHandlingKeyboardScrolling && !IsDragging)
259 {
260 switch (e.Key)
261 {
262 case Key.PageUp:
263 OnUserScroll(Target - DisplayableContent);
264 return true;
265
266 case Key.PageDown:
267 OnUserScroll(Target + DisplayableContent);
268 return true;
269 }
270 }
271
272 return base.OnKeyDown(e);
273 }
274
275 protected override bool OnMouseDown(MouseDownEvent e)
276 {
277 if (IsDragging || e.Button != MouseButton.Left) return false;
278
279 // Continue from where we currently are scrolled to.
280 Target = Current;
281
282 return true;
283 }
284
285 // We keep track of this because input events may happen at different intervals than update frames
286 // and we are interested in the time difference between drag _input_ events.
287 private double lastDragTime;
288
289 // These keep track of a sliding average (w.r.t. time) of the time between drag events
290 // and the delta of drag events. Both of these moving averages are decayed at the same
291 // rate and thus the velocity remains constant across time. The overall magnitude
292 // of averageDragTime and averageDragDelta simple decreases such that more recent movements
293 // have a larger weight.
294 private double averageDragTime;
295 private double averageDragDelta;
296
297 private MouseButtonEventManager dragButtonManager;
298
299 private bool dragBlocksClick;
300
301 public override bool DragBlocksClick => dragBlocksClick;
302
303 protected override void OnDrag(DragEvent e)
304 {
305 Trace.Assert(IsDragging, "We should never receive OnDrag if we are not dragging.");
306
307 double currentTime = Time.Current;
308 double timeDelta = currentTime - lastDragTime;
309 double decay = Math.Pow(0.95, timeDelta);
310
311 averageDragTime = averageDragTime * decay + timeDelta;
312 averageDragDelta = averageDragDelta * decay - e.Delta[ScrollDim];
313
314 lastDragTime = currentTime;
315
316 Vector2 childDelta = ToLocalSpace(e.ScreenSpaceMousePosition) - ToLocalSpace(e.ScreenSpaceLastMousePosition);
317
318 float scrollOffset = -childDelta[ScrollDim];
319 float clampedScrollOffset = Clamp(Target + scrollOffset) - Clamp(Target);
320
321 Debug.Assert(Precision.AlmostBigger(Math.Abs(scrollOffset), clampedScrollOffset * Math.Sign(scrollOffset)));
322
323 // If we are dragging past the extent of the scrollable area, half the offset
324 // such that the user can feel it.
325 scrollOffset = clampedScrollOffset + (scrollOffset - clampedScrollOffset) / 2;
326
327 // similar calculation to what is already done in MouseButtonEventManager.HandlePositionChange
328 // handles the case where a drag was triggered on an axis we are not interested in.
329 // can be removed if/when drag events are split out per axis or contain direction information.
330 dragBlocksClick |= Math.Abs(e.MouseDownPosition[ScrollDim] - e.MousePosition[ScrollDim]) > dragButtonManager.ClickDragDistance;
331
332 scrollByOffset(scrollOffset, false);
333 }
334
335 protected override void OnDragEnd(DragEndEvent e)
336 {
337 Trace.Assert(IsDragging, "We should never receive OnDragEnd if we are not dragging.");
338
339 dragBlocksClick = false;
340 dragButtonManager = null;
341 IsDragging = false;
342
343 if (averageDragTime <= 0.0)
344 return;
345
346 double velocity = averageDragDelta / averageDragTime;
347
348 // Detect whether we halted at the end of the drag and in fact should _not_
349 // perform a flick event.
350 const double velocity_cutoff = 0.1;
351 if (Math.Abs(Math.Pow(0.95, Time.Current - lastDragTime) * velocity) < velocity_cutoff)
352 velocity = 0;
353
354 // Differentiate f(t) = distance * (1 - exp(-t)) w.r.t. "t" to obtain
355 // velocity w.r.t. time. Then rearrange to solve for distance given velocity.
356 double distance = velocity / (1 - Math.Exp(-DistanceDecayDrag));
357
358 scrollByOffset((float)distance, true, DistanceDecayDrag);
359 }
360
361 protected override bool OnScroll(ScrollEvent e)
362 {
363 if (Content.AliveInternalChildren.Count == 0)
364 return false;
365
366 bool isPrecise = e.IsPrecise;
367
368 Vector2 scrollDelta = e.ScrollDelta;
369 float scrollDeltaFloat = scrollDelta.Y;
370 if (ScrollDirection == Direction.Horizontal && scrollDelta.X != 0)
371 scrollDeltaFloat = scrollDelta.X;
372
373 scrollByOffset((isPrecise ? 10 : ScrollDistance) * -scrollDeltaFloat, true, isPrecise ? 0.05 : DistanceDecayScroll);
374 return true;
375 }
376
377 private void onScrollbarMovement(float value) => OnUserScroll(Clamp(fromScrollbarPosition(value)), false);
378
379 /// <summary>
380 /// Immediately offsets the current and target scroll position.
381 /// </summary>
382 /// <param name="offset">The scroll offset.</param>
383 public void OffsetScrollPosition(float offset)
384 {
385 Target += offset;
386 Current += offset;
387 }
388
389 private void scrollByOffset(float value, bool animated, double distanceDecay = float.PositiveInfinity) =>
390 OnUserScroll(Target + value, animated, distanceDecay);
391
392 /// <summary>
393 /// Scroll to the start of available content.
394 /// </summary>
395 /// <param name="animated">Whether to animate the movement.</param>
396 /// <param name="allowDuringDrag">Whether we should interrupt a user's active drag.</param>
397 public void ScrollToStart(bool animated = true, bool allowDuringDrag = false)
398 {
399 if (!IsDragging || allowDuringDrag)
400 scrollTo(0, animated, DistanceDecayJump);
401 }
402
403 /// <summary>
404 /// Scroll to the end of available content.
405 /// </summary>
406 /// <param name="animated">Whether to animate the movement.</param>
407 /// <param name="allowDuringDrag">Whether we should interrupt a user's active drag.</param>
408 public void ScrollToEnd(bool animated = true, bool allowDuringDrag = false)
409 {
410 if (!IsDragging || allowDuringDrag)
411 scrollTo(ScrollableExtent, animated, DistanceDecayJump);
412 }
413
414 /// <summary>
415 /// Scrolls to a new position relative to the current scroll offset.
416 /// </summary>
417 /// <param name="offset">The amount by which we should scroll.</param>
418 /// <param name="animated">Whether to animate the movement.</param>
419 public void ScrollBy(float offset, bool animated = true) => scrollTo(Target + offset, animated);
420
421 /// <summary>
422 /// Handle a scroll to an absolute position from a user input.
423 /// </summary>
424 /// <param name="value">The position to scroll to.</param>
425 /// <param name="animated">Whether to animate the movement.</param>
426 /// <param name="distanceDecay">Controls the rate with which the target position is approached after jumping to a specific location. Default is <see cref="DistanceDecayJump"/>.</param>
427 protected virtual void OnUserScroll(float value, bool animated = true, double? distanceDecay = null) =>
428 ScrollTo(value, animated, distanceDecay);
429
430 /// <summary>
431 /// Scrolls to an absolute position.
432 /// </summary>
433 /// <param name="value">The position to scroll to.</param>
434 /// <param name="animated">Whether to animate the movement.</param>
435 /// <param name="distanceDecay">Controls the rate with which the target position is approached after jumping to a specific location. Default is <see cref="DistanceDecayJump"/>.</param>
436 public void ScrollTo(float value, bool animated = true, double? distanceDecay = null) => scrollTo(value, animated, distanceDecay ?? DistanceDecayJump);
437
438 private void scrollTo(float value, bool animated, double distanceDecay = float.PositiveInfinity)
439 {
440 Target = Clamp(value, ClampExtension);
441
442 if (animated)
443 this.distanceDecay = distanceDecay;
444 else
445 Current = Target;
446 }
447
448 /// <summary>
449 /// Scrolls a <see cref="Drawable"/> to the top.
450 /// </summary>
451 /// <param name="d">The <see cref="Drawable"/> to scroll to.</param>
452 /// <param name="animated">Whether to animate the movement.</param>
453 public void ScrollTo(Drawable d, bool animated = true) => ScrollTo(GetChildPosInContent(d), animated);
454
455 /// <summary>
456 /// Scrolls a <see cref="Drawable"/> into view.
457 /// </summary>
458 /// <param name="d">The <see cref="Drawable"/> to scroll into view.</param>
459 /// <param name="animated">Whether to animate the movement.</param>
460 public void ScrollIntoView(Drawable d, bool animated = true)
461 {
462 float childPos0 = GetChildPosInContent(d);
463 float childPos1 = GetChildPosInContent(d, d.DrawSize);
464
465 float minPos = Math.Min(childPos0, childPos1);
466 float maxPos = Math.Max(childPos0, childPos1);
467
468 if (minPos < Current || (minPos > Current && d.DrawSize[ScrollDim] > DisplayableContent))
469 ScrollTo(minPos, animated);
470 else if (maxPos > Current + DisplayableContent)
471 ScrollTo(maxPos - DisplayableContent, animated);
472 }
473
474 /// <summary>
475 /// Determines the position of a child in the content.
476 /// </summary>
477 /// <param name="d">The child to get the position from.</param>
478 /// <param name="offset">Positional offset in the child's space.</param>
479 /// <returns>The position of the child.</returns>
480 public float GetChildPosInContent(Drawable d, Vector2 offset) => d.ToSpaceOfOtherDrawable(offset, ScrollContent)[ScrollDim];
481
482 /// <summary>
483 /// Determines the position of a child in the content.
484 /// </summary>
485 /// <param name="d">The child to get the position from.</param>
486 /// <returns>The position of the child.</returns>
487 public float GetChildPosInContent(Drawable d) => GetChildPosInContent(d, Vector2.Zero);
488
489 private void updatePosition()
490 {
491 double localDistanceDecay = distanceDecay;
492
493 // If we are not currently dragging the content, and we have scrolled out of bounds,
494 // then we should handle the clamping force. Note, that if the target is _within_
495 // acceptable bounds, then we do not need special handling of the clamping force, as
496 // we will naturally scroll back into acceptable bounds.
497 if (!IsDragging && Current != Clamp(Current) && Target != Clamp(Target, -0.01f))
498 {
499 // Firstly, we want to limit how far out the target may go to limit overly bouncy
500 // behaviour with extreme scroll velocities.
501 Target = Clamp(Target, ClampExtension);
502
503 // Secondly, we would like to quickly approach the target while we are out of bounds.
504 // This is simulating a "strong" clamping force towards the target.
505 if (Current < Target && Target < 0 || Current > Target && Target > ScrollableExtent)
506 localDistanceDecay = distance_decay_clamping * 2;
507
508 // Lastly, we gradually nudge the target towards valid bounds.
509 Target = (float)Interpolation.Lerp(Clamp(Target), Target, Math.Exp(-distance_decay_clamping * Time.Elapsed));
510
511 float clampedTarget = Clamp(Target);
512 if (Precision.AlmostEquals(clampedTarget, Target))
513 Target = clampedTarget;
514 }
515
516 // Exponential interpolation between the target and our current scroll position.
517 Current = (float)Interpolation.Lerp(Target, Current, Math.Exp(-localDistanceDecay * Time.Elapsed));
518
519 // This prevents us from entering the de-normalized range of floating point numbers when approaching target closely.
520 if (Precision.AlmostEquals(Current, Target))
521 Current = Target;
522 }
523
524 protected override void UpdateAfterChildren()
525 {
526 base.UpdateAfterChildren();
527
528 updateSize();
529 updatePosition();
530
531 if (!scrollbarCache.IsValid)
532 {
533 var size = ScrollDirection == Direction.Horizontal ? DrawWidth : DrawHeight;
534 if (size > 0)
535 Scrollbar.ResizeTo(Math.Clamp(AvailableContent > 0 ? DisplayableContent / AvailableContent : 0, Math.Min(Scrollbar.MinimumDimSize / size, 1), 1), 200, Easing.OutQuint);
536 Scrollbar.FadeTo(ScrollbarVisible && AvailableContent - 1 > DisplayableContent ? 1 : 0, 200);
537 updatePadding();
538
539 scrollbarCache.Validate();
540 }
541
542 if (ScrollDirection == Direction.Horizontal)
543 {
544 Scrollbar.X = toScrollbarPosition(Current);
545 ScrollContent.X = -Current + ScrollableExtent * ScrollContent.RelativeAnchorPosition.X;
546 }
547 else
548 {
549 Scrollbar.Y = toScrollbarPosition(Current);
550 ScrollContent.Y = -Current + ScrollableExtent * ScrollContent.RelativeAnchorPosition.Y;
551 }
552 }
553
554 /// <summary>
555 /// Converts a scroll position to a scrollbar position.
556 /// </summary>
557 /// <param name="scrollPosition">The absolute scroll position (e.g. <see cref="Current"/>).</param>
558 /// <returns>The scrollbar position.</returns>
559 private float toScrollbarPosition(float scrollPosition)
560 {
561 if (Precision.AlmostEquals(0, ScrollableExtent))
562 return 0;
563
564 return ScrollbarMovementExtent * (scrollPosition / ScrollableExtent);
565 }
566
567 /// <summary>
568 /// Converts a scrollbar position to a scroll position.
569 /// </summary>
570 /// <param name="scrollbarPosition">The scrollbar position.</param>
571 /// <returns>The absolute scroll position.</returns>
572 private float fromScrollbarPosition(float scrollbarPosition)
573 {
574 if (Precision.AlmostEquals(0, ScrollbarMovementExtent))
575 return 0;
576
577 return ScrollableExtent * (scrollbarPosition / ScrollbarMovementExtent);
578 }
579
580 /// <summary>
581 /// Creates the scrollbar for this <see cref="ScrollContainer{T}"/>.
582 /// </summary>
583 /// <param name="direction">The scrolling direction.</param>
584 protected abstract ScrollbarContainer CreateScrollbar(Direction direction);
585
586 protected internal abstract class ScrollbarContainer : Container
587 {
588 private float dragOffset;
589
590 internal Action<float> Dragged;
591
592 protected readonly Direction ScrollDirection;
593
594 /// <summary>
595 /// The minimum size of this <see cref="ScrollbarContainer"/>. Defaults to the size in the non-scrolling direction.
596 /// </summary>
597 protected internal virtual float MinimumDimSize => Size[ScrollDirection == Direction.Vertical ? 0 : 1];
598
599 protected ScrollbarContainer(Direction direction)
600 {
601 ScrollDirection = direction;
602
603 RelativeSizeAxes = direction == Direction.Horizontal ? Axes.X : Axes.Y;
604 }
605
606 public abstract void ResizeTo(float val, int duration = 0, Easing easing = Easing.None);
607
608 protected override bool OnClick(ClickEvent e) => true;
609
610 protected override bool OnDragStart(DragStartEvent e)
611 {
612 if (e.Button != MouseButton.Left) return false;
613
614 dragOffset = e.MousePosition[(int)ScrollDirection] - Position[(int)ScrollDirection];
615 return true;
616 }
617
618 protected override bool OnMouseDown(MouseDownEvent e)
619 {
620 if (e.Button != MouseButton.Left) return false;
621
622 dragOffset = Position[(int)ScrollDirection];
623 Dragged?.Invoke(dragOffset);
624 return true;
625 }
626
627 protected override void OnDrag(DragEvent e)
628 {
629 Dragged?.Invoke(e.MousePosition[(int)ScrollDirection] - dragOffset);
630 }
631 }
632
633 public bool OnPressed(PlatformAction action)
634 {
635 if (!IsHandlingKeyboardScrolling)
636 return false;
637
638 switch (action)
639 {
640 case PlatformAction.MoveBackwardLine:
641 ScrollToStart();
642 return true;
643
644 case PlatformAction.MoveForwardLine:
645 ScrollToEnd();
646 return true;
647
648 default:
649 return false;
650 }
651 }
652
653 public void OnReleased(PlatformAction action)
654 {
655 }
656 }
657}