diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index bd483e073c..096de558dd 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -15,6 +15,7 @@ using osu.Framework.Threading; using osu.Game.Graphics.Containers; using osu.Game.Overlays.Notifications; using osu.Game.Resources.Localisation.Web; +using osuTK; using NotificationsStrings = osu.Game.Localisation.NotificationsStrings; namespace osu.Game.Overlays @@ -39,6 +40,23 @@ namespace osu.Game.Overlays private readonly IBindable firstRunSetupVisibility = new Bindable(); + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) + { + if (State.Value == Visibility.Visible) + return base.ReceivePositionalInputAt(screenSpacePos); + + if (toastTray.IsDisplayingToasts) + return toastTray.ReceivePositionalInputAt(screenSpacePos); + + return false; + } + + public override bool PropagatePositionalInputSubTree => base.PropagatePositionalInputSubTree || toastTray.IsDisplayingToasts; + + private NotificationOverlayToastTray toastTray = null!; + + private Container mainContent = null!; + [BackgroundDependencyLoader] private void load(FirstRunSetupOverlay? firstRunSetup) { @@ -48,30 +66,41 @@ namespace osu.Game.Overlays Children = new Drawable[] { - new Box + toastTray = new NotificationOverlayToastTray { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background4, + Origin = Anchor.TopRight, }, - new OsuScrollContainer + mainContent = new Container { - Masking = true, RelativeSizeAxes = Axes.Both, - Children = new[] + Children = new Drawable[] { - sections = new FillFlowContainer + new Box { - Direction = FillDirection.Vertical, - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4, + }, + new OsuScrollContainer + { + Masking = true, + RelativeSizeAxes = Axes.Both, Children = new[] { - new NotificationSection(AccountsStrings.NotificationsTitle, new[] { typeof(SimpleNotification) }, "Clear All"), - new NotificationSection(@"Running Tasks", new[] { typeof(ProgressNotification) }, @"Cancel All"), + sections = new FillFlowContainer + { + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Children = new[] + { + new NotificationSection(AccountsStrings.NotificationsTitle, new[] { typeof(SimpleNotification) }, "Clear All"), + new NotificationSection(@"Running Tasks", new[] { typeof(ProgressNotification) }, @"Cancel All"), + } + } } } } - } + }, }; if (firstRunSetup != null) @@ -129,14 +158,22 @@ namespace osu.Game.Overlays var ourType = notification.GetType(); - var section = sections.Children.FirstOrDefault(s => s.AcceptedNotificationTypes.Any(accept => accept.IsAssignableFrom(ourType))); - section?.Add(notification, notification.DisplayOnTop ? -runningDepth : runningDepth); + int depth = notification.DisplayOnTop ? -runningDepth : runningDepth; - if (notification.IsImportant) - Show(); + if (State.Value == Visibility.Hidden) + toastTray.Post(notification, addPermanently); + else + addPermanently(); - updateCounts(); - playDebouncedSample(notification.PopInSampleName); + void addPermanently() + { + var section = sections.Children.First(s => s.AcceptedNotificationTypes.Any(accept => accept.IsAssignableFrom(ourType))); + + section.Add(notification, depth); + + updateCounts(); + playDebouncedSample(notification.PopInSampleName); + } }); protected override void Update() @@ -152,7 +189,9 @@ namespace osu.Game.Overlays base.PopIn(); this.MoveToX(0, TRANSITION_LENGTH, Easing.OutQuint); - this.FadeTo(1, TRANSITION_LENGTH, Easing.OutQuint); + mainContent.FadeTo(1, TRANSITION_LENGTH, Easing.OutQuint); + + toastTray.FlushAllToasts(); } protected override void PopOut() @@ -162,7 +201,7 @@ namespace osu.Game.Overlays markAllRead(); this.MoveToX(WIDTH, TRANSITION_LENGTH, Easing.OutQuint); - this.FadeTo(0, TRANSITION_LENGTH, Easing.OutQuint); + mainContent.FadeTo(0, TRANSITION_LENGTH, Easing.OutQuint); } private void notificationClosed() diff --git a/osu.Game/Overlays/NotificationOverlayToastTray.cs b/osu.Game/Overlays/NotificationOverlayToastTray.cs new file mode 100644 index 0000000000..338e84c472 --- /dev/null +++ b/osu.Game/Overlays/NotificationOverlayToastTray.cs @@ -0,0 +1,124 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Threading; +using osu.Framework.Utils; +using osu.Game.Overlays.Notifications; +using osuTK; + +namespace osu.Game.Overlays +{ + /// + /// A tray which attaches to the left of to show temporary toasts. + /// + public class NotificationOverlayToastTray : CompositeDrawable + { + public bool IsDisplayingToasts => toastFlow.Count > 0; + + private FillFlowContainer toastFlow = null!; + private BufferedContainer toastContentBackground = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + private readonly List pendingToastOperations = new List(); + + private int runningDepth; + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Padding = new MarginPadding(20); + + InternalChildren = new Drawable[] + { + toastContentBackground = (new Box + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Colour = ColourInfo.GradientVertical( + colourProvider.Background6.Opacity(0.7f), + colourProvider.Background6.Opacity(0.5f)), + RelativeSizeAxes = Axes.Both, + }.WithEffect(new BlurEffect + { + PadExtent = true, + Sigma = new Vector2(20), + }).With(postEffectDrawable => + { + postEffectDrawable.Scale = new Vector2(1.5f, 1); + postEffectDrawable.Position += new Vector2(70, -50); + postEffectDrawable.AutoSizeAxes = Axes.None; + postEffectDrawable.RelativeSizeAxes = Axes.X; + })), + toastFlow = new FillFlowContainer + { + LayoutDuration = 150, + LayoutEasing = Easing.OutQuart, + Spacing = new Vector2(3), + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }, + }; + } + + public void FlushAllToasts() + { + foreach (var d in pendingToastOperations.Where(d => !d.Completed)) + d.RunTask(); + + pendingToastOperations.Clear(); + } + + public void Post(Notification notification, Action addPermanently) + { + ++runningDepth; + + int depth = notification.DisplayOnTop ? -runningDepth : runningDepth; + + toastFlow.Insert(depth, notification); + + pendingToastOperations.Add(Scheduler.AddDelayed(() => + { + // add notification to permanent overlay unless it was already dismissed by the user. + if (notification.WasClosed) + return; + + toastFlow.Remove(notification); + AddInternal(notification); + + notification.MoveToOffset(new Vector2(400, 0), NotificationOverlay.TRANSITION_LENGTH, Easing.OutQuint); + notification.FadeOut(NotificationOverlay.TRANSITION_LENGTH, Easing.OutQuint).OnComplete(_ => + { + RemoveInternal(notification); + addPermanently(); + + notification.FadeIn(300, Easing.OutQuint); + }); + }, notification.IsImportant ? 15000 : 3000)); + } + + protected override void Update() + { + base.Update(); + + float height = toastFlow.DrawHeight + 120; + float alpha = IsDisplayingToasts ? MathHelper.Clamp(toastFlow.DrawHeight / 40, 0, 1) * toastFlow.Children.Max(n => n.Alpha) : 0; + + toastContentBackground.Height = (float)Interpolation.DampContinuously(toastContentBackground.Height, height, 10, Clock.ElapsedFrameTime); + toastContentBackground.Alpha = (float)Interpolation.DampContinuously(toastContentBackground.Alpha, alpha, 10, Clock.ElapsedFrameTime); + } + } +}