diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs index 5effc1f215..f835d21603 100644 --- a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs +++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs @@ -6,12 +6,14 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables.Cards; +using osu.Game.Graphics.Containers; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; @@ -227,7 +229,7 @@ namespace osu.Game.Tests.Visual.Beatmaps new BasicScrollContainer { RelativeSizeAxes = Axes.Both, - Child = new FillFlowContainer + Child = new ReverseChildIDFillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -248,6 +250,17 @@ namespace osu.Game.Tests.Visual.Beatmaps } [Test] - public void TestNormal() => createTestCase(beatmapSetInfo => new BeatmapCard(beatmapSetInfo)); + public void TestNormal() + { + createTestCase(beatmapSetInfo => new BeatmapCard(beatmapSetInfo)); + + AddToggleStep("toggle expanded state", expanded => + { + var card = this.ChildrenOfType().Last(); + if (!card.Expanded.Disabled) + card.Expanded.Value = expanded; + }); + AddToggleStep("disable/enable expansion", disabled => this.ChildrenOfType().ForEach(card => card.Expanded.Disabled = disabled)); + } } } diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs index 37c1bacda4..9031d6df1a 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs @@ -31,10 +31,12 @@ namespace osu.Game.Beatmaps.Drawables.Cards public class BeatmapCard : OsuClickableContainer { public const float TRANSITION_DURATION = 400; + public const float CORNER_RADIUS = 10; + + public Bindable Expanded { get; } = new BindableBool(); private const float width = 408; private const float height = 100; - private const float corner_radius = 10; private const float icon_area_width = 30; private readonly APIBeatmapSet beatmapSet; @@ -42,6 +44,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards private readonly BeatmapDownloadTracker downloadTracker; + private BeatmapCardContent content = null!; + private BeatmapCardThumbnail thumbnail = null!; private Container rightAreaBackground = null!; @@ -73,242 +77,241 @@ namespace osu.Game.Beatmaps.Drawables.Cards { Width = width; Height = height; - CornerRadius = corner_radius; - Masking = true; FillFlowContainer leftIconArea; GridContainer titleContainer; GridContainer artistContainer; - InternalChildren = new Drawable[] + InternalChild = content = new BeatmapCardContent(height) { - downloadTracker, - rightAreaBackground = new Container + MainContent = new Container { - RelativeSizeAxes = Axes.Y, - Width = icon_area_width + 2 * corner_radius, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - // workaround for masking artifacts at the top & bottom of card, - // which become especially visible on downloaded beatmaps (when the icon area has a lime background). - Padding = new MarginPadding { Vertical = 1 }, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Colour4.White - }, - }, - thumbnail = new BeatmapCardThumbnail(beatmapSet) - { - Name = @"Left (icon) area", - Size = new Vector2(height), - Padding = new MarginPadding { Right = corner_radius }, - Child = leftIconArea = new FillFlowContainer - { - Margin = new MarginPadding(5), - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(1) - } - }, - new Container - { - Name = @"Right (button) area", - Width = 30, - RelativeSizeAxes = Axes.Y, - Origin = Anchor.TopRight, - Anchor = Anchor.TopRight, - Padding = new MarginPadding { Vertical = 17.5f }, - Child = rightAreaButtons = new Container - { - RelativeSizeAxes = Axes.Both, - Children = new BeatmapCardIconButton[] - { - new FavouriteButton(beatmapSet) - { - Current = favouriteState, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre - }, - new DownloadButton(beatmapSet) - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - State = { BindTarget = downloadTracker.State } - }, - new GoToBeatmapButton(beatmapSet) - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - State = { BindTarget = downloadTracker.State } - } - } - } - }, - mainContent = new Container - { - Name = @"Main content", - X = height - corner_radius, - Height = height, - CornerRadius = corner_radius, - Masking = true, + RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - mainContentBackground = new BeatmapCardContentBackground(beatmapSet) + downloadTracker, + rightAreaBackground = new Container { - RelativeSizeAxes = Axes.Both, - }, - new FillFlowContainer - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding + RelativeSizeAxes = Axes.Y, + Width = icon_area_width + 2 * CORNER_RADIUS, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + // workaround for masking artifacts at the top & bottom of card, + // which become especially visible on downloaded beatmaps (when the icon area has a lime background). + Padding = new MarginPadding { Vertical = 1 }, + Child = new Box { - Horizontal = 10, - Vertical = 4 + RelativeSizeAxes = Axes.Both, + Colour = Colour4.White }, - Direction = FillDirection.Vertical, - Children = new Drawable[] + }, + thumbnail = new BeatmapCardThumbnail(beatmapSet) + { + Name = @"Left (icon) area", + Size = new Vector2(height), + Padding = new MarginPadding { Right = CORNER_RADIUS }, + Child = leftIconArea = new FillFlowContainer { - titleContainer = new GridContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - ColumnDimensions = new[] - { - new Dimension(), - new Dimension(GridSizeMode.AutoSize) - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize) - }, - Content = new[] - { - new[] - { - new OsuSpriteText - { - Text = new RomanisableString(beatmapSet.TitleUnicode, beatmapSet.Title), - Font = OsuFont.Default.With(size: 22.5f, weight: FontWeight.SemiBold), - RelativeSizeAxes = Axes.X, - Truncate = true - }, - Empty() - } - } - }, - artistContainer = new GridContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - ColumnDimensions = new[] - { - new Dimension(), - new Dimension(GridSizeMode.AutoSize) - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize) - }, - Content = new[] - { - new[] - { - new OsuSpriteText - { - Text = createArtistText(), - Font = OsuFont.Default.With(size: 17.5f, weight: FontWeight.SemiBold), - RelativeSizeAxes = Axes.X, - Truncate = true - }, - Empty() - }, - } - }, - new LinkFlowContainer(s => - { - s.Shadow = false; - s.Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold); - }).With(d => - { - d.AutoSizeAxes = Axes.Both; - d.Margin = new MarginPadding { Top = 2 }; - d.AddText("mapped by ", t => t.Colour = colourProvider.Content2); - d.AddUserLink(beatmapSet.Author); - }), + Margin = new MarginPadding(5), + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(1) } }, new Container { - Name = @"Bottom content", - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Padding = new MarginPadding + Name = @"Right (button) area", + Width = 30, + RelativeSizeAxes = Axes.Y, + Origin = Anchor.TopRight, + Anchor = Anchor.TopRight, + Padding = new MarginPadding { Vertical = 17.5f }, + Child = rightAreaButtons = new Container { - Horizontal = 10, - Vertical = 4 - }, + RelativeSizeAxes = Axes.Both, + Children = new BeatmapCardIconButton[] + { + new FavouriteButton(beatmapSet) + { + Current = favouriteState, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre + }, + new DownloadButton(beatmapSet) + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + State = { BindTarget = downloadTracker.State } + }, + new GoToBeatmapButton(beatmapSet) + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + State = { BindTarget = downloadTracker.State } + } + } + } + }, + mainContent = new Container + { + Name = @"Main content", + X = height - CORNER_RADIUS, + Height = height, + CornerRadius = CORNER_RADIUS, + Masking = true, Children = new Drawable[] { - idleBottomContent = new FillFlowContainer + mainContentBackground = new BeatmapCardContentBackground(beatmapSet) { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.Both, + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding + { + Horizontal = 10, + Vertical = 4 + }, Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 3), - AlwaysPresent = true, Children = new Drawable[] { - statisticsContainer = new FillFlowContainer + titleContainer = new GridContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), - Alpha = 0, - AlwaysPresent = true, - ChildrenEnumerable = createStatistics() - }, - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(4, 0), - Children = new Drawable[] + ColumnDimensions = new[] { - new BeatmapSetOnlineStatusPill + new Dimension(), + new Dimension(GridSizeMode.AutoSize) + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new[] { - AutoSizeAxes = Axes.Both, - Status = beatmapSet.Status, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft - }, - new DifficultySpectrumDisplay(beatmapSet) - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - DotSize = new Vector2(6, 12) + new OsuSpriteText + { + Text = new RomanisableString(beatmapSet.TitleUnicode, beatmapSet.Title), + Font = OsuFont.Default.With(size: 22.5f, weight: FontWeight.SemiBold), + RelativeSizeAxes = Axes.X, + Truncate = true + }, + Empty() } } - } + }, + artistContainer = new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize) + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new[] + { + new OsuSpriteText + { + Text = createArtistText(), + Font = OsuFont.Default.With(size: 17.5f, weight: FontWeight.SemiBold), + RelativeSizeAxes = Axes.X, + Truncate = true + }, + Empty() + }, + } + }, + new LinkFlowContainer(s => + { + s.Shadow = false; + s.Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold); + }).With(d => + { + d.AutoSizeAxes = Axes.Both; + d.Margin = new MarginPadding { Top = 2 }; + d.AddText("mapped by ", t => t.Colour = colourProvider.Content2); + d.AddUserLink(beatmapSet.Author); + }), } }, - downloadProgressBar = new BeatmapCardDownloadProgressBar + new Container { + Name = @"Bottom content", RelativeSizeAxes = Axes.X, - Height = 6, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - State = { BindTarget = downloadTracker.State }, - Progress = { BindTarget = downloadTracker.Progress } + AutoSizeAxes = Axes.Y, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Padding = new MarginPadding + { + Horizontal = 10, + Vertical = 4 + }, + Children = new Drawable[] + { + idleBottomContent = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 3), + AlwaysPresent = true, + Children = new Drawable[] + { + statisticsContainer = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), + Alpha = 0, + AlwaysPresent = true, + ChildrenEnumerable = createStatistics() + }, + new BeatmapCardExtraInfoRow(beatmapSet) + { + Hovered = _ => + { + content.ScheduleShow(); + return false; + }, + Unhovered = _ => content.ScheduleHide() + } + } + }, + downloadProgressBar = new BeatmapCardDownloadProgressBar + { + RelativeSizeAxes = Axes.X, + Height = 6, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + State = { BindTarget = downloadTracker.State }, + Progress = { BindTarget = downloadTracker.Progress } + } + } } } } } - } + }, + ExpandedContent = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = 10, Vertical = 13 }, + Child = new BeatmapCardDifficultyList(beatmapSet) + }, + Expanded = { BindTarget = Expanded } }; if (beatmapSet.HasVideo) @@ -344,7 +347,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards { base.LoadComplete(); - downloadTracker.State.BindValueChanged(_ => updateState(), true); + downloadTracker.State.BindValueChanged(_ => updateState()); + Expanded.BindValueChanged(_ => updateState(), true); FinishTransforms(true); } @@ -386,19 +390,21 @@ namespace osu.Game.Beatmaps.Drawables.Cards private void updateState() { - float targetWidth = width - height; - if (IsHovered) - targetWidth = targetWidth - icon_area_width + corner_radius; + bool showDetails = IsHovered || Expanded.Value; - thumbnail.Dimmed.Value = IsHovered; + float targetWidth = width - height; + if (showDetails) + targetWidth = targetWidth - icon_area_width + CORNER_RADIUS; + + thumbnail.Dimmed.Value = showDetails; mainContent.ResizeWidthTo(targetWidth, TRANSITION_DURATION, Easing.OutQuint); - mainContentBackground.Dimmed.Value = IsHovered; + mainContentBackground.Dimmed.Value = showDetails; - statisticsContainer.FadeTo(IsHovered ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint); + statisticsContainer.FadeTo(showDetails ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint); rightAreaBackground.FadeColour(downloadTracker.State.Value == DownloadState.LocallyAvailable ? colours.Lime0 : colourProvider.Background3, TRANSITION_DURATION, Easing.OutQuint); - rightAreaButtons.FadeTo(IsHovered ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint); + rightAreaButtons.FadeTo(showDetails ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint); foreach (var button in rightAreaButtons) { diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs new file mode 100644 index 0000000000..7f94d0e3b7 --- /dev/null +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs @@ -0,0 +1,231 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using System; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Framework.Threading; +using osu.Framework.Utils; +using osu.Game.Graphics.Containers; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Beatmaps.Drawables.Cards +{ + public class BeatmapCardContent : CompositeDrawable + { + public Drawable MainContent + { + set => bodyContent.Child = value; + } + + public Drawable ExpandedContent + { + set => dropdownScroll.Child = value; + } + + public Bindable Expanded { get; } = new BindableBool(); + + private readonly Box background; + private readonly Container content; + private readonly Container bodyContent; + private readonly Container dropdownContent; + private readonly OsuScrollContainer dropdownScroll; + private readonly Container borderContainer; + + public BeatmapCardContent(float height) + { + RelativeSizeAxes = Axes.X; + Height = height; + + InternalChild = content = new HoverHandlingContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + CornerRadius = BeatmapCard.CORNER_RADIUS, + Masking = true, + Unhovered = _ => checkForHide(), + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both + }, + bodyContent = new Container + { + RelativeSizeAxes = Axes.X, + Height = height, + CornerRadius = BeatmapCard.CORNER_RADIUS, + Masking = true, + }, + dropdownContent = new HoverHandlingContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Top = height }, + Alpha = 0, + Hovered = _ => + { + keep(); + return true; + }, + Unhovered = _ => checkForHide(), + Child = dropdownScroll = new ExpandedContentScrollContainer + { + RelativeSizeAxes = Axes.X, + ScrollbarVisible = false + } + }, + borderContainer = new Container + { + RelativeSizeAxes = Axes.Both, + CornerRadius = BeatmapCard.CORNER_RADIUS, + Masking = true, + BorderThickness = 3, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + } + } + } + }; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + background.Colour = colourProvider.Background2; + borderContainer.BorderColour = colourProvider.Highlight1; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Expanded.BindValueChanged(_ => updateState(), true); + FinishTransforms(true); + } + + private ScheduledDelegate? scheduledExpandedChange; + + public void ScheduleShow() + { + scheduledExpandedChange?.Cancel(); + if (Expanded.Disabled || Expanded.Value) + return; + + scheduledExpandedChange = Scheduler.AddDelayed(() => + { + if (!Expanded.Disabled) + Expanded.Value = true; + }, 100); + } + + public void ScheduleHide() + { + scheduledExpandedChange?.Cancel(); + if (Expanded.Disabled || !Expanded.Value) + return; + + scheduledExpandedChange = Scheduler.AddDelayed(() => + { + if (!Expanded.Disabled) + Expanded.Value = false; + }, 500); + } + + private void checkForHide() + { + if (Expanded.Disabled) + return; + + if (content.IsHovered || dropdownContent.IsHovered) + return; + + scheduledExpandedChange?.Cancel(); + Expanded.Value = false; + } + + private void keep() + { + if (Expanded.Disabled) + return; + + scheduledExpandedChange?.Cancel(); + Expanded.Value = true; + } + + private void updateState() + { + background.FadeTo(Expanded.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + dropdownContent.FadeTo(Expanded.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + borderContainer.FadeTo(Expanded.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + + content.TweenEdgeEffectTo(new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Offset = new Vector2(0, 2), + Radius = 10, + Colour = Colour4.Black.Opacity(Expanded.Value ? 0.3f : 0f), + Hollow = true, + }, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + } + + private class ExpandedContentScrollContainer : OsuScrollContainer + { + public ExpandedContentScrollContainer() + { + ScrollbarVisible = false; + } + + protected override void Update() + { + base.Update(); + + Height = Math.Min(Content.DrawHeight, 400); + } + + private bool allowScroll => !Precision.AlmostEquals(DrawSize, Content.DrawSize); + + protected override bool OnDragStart(DragStartEvent e) + { + if (!allowScroll) + return false; + + return base.OnDragStart(e); + } + + protected override void OnDrag(DragEvent e) + { + if (!allowScroll) + return; + + base.OnDrag(e); + } + + protected override void OnDragEnd(DragEndEvent e) + { + if (!allowScroll) + return; + + base.OnDragEnd(e); + } + + protected override bool OnScroll(ScrollEvent e) + { + if (!allowScroll) + return false; + + return base.OnScroll(e); + } + } + } +} diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs new file mode 100644 index 0000000000..0a9d98e621 --- /dev/null +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs @@ -0,0 +1,43 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Online.API.Requests.Responses; +using osuTK; + +namespace osu.Game.Beatmaps.Drawables.Cards +{ + public class BeatmapCardExtraInfoRow : HoverHandlingContainer + { + public BeatmapCardExtraInfoRow(APIBeatmapSet beatmapSet) + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(4, 0), + Children = new Drawable[] + { + new BeatmapSetOnlineStatusPill + { + AutoSizeAxes = Axes.Both, + Status = beatmapSet.Status, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft + }, + new DifficultySpectrumDisplay(beatmapSet) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + DotSize = new Vector2(6, 12) + } + } + }; + } + } +} diff --git a/osu.Game/Beatmaps/Drawables/Cards/HoverHandlingContainer.cs b/osu.Game/Beatmaps/Drawables/Cards/HoverHandlingContainer.cs new file mode 100644 index 0000000000..fe37616755 --- /dev/null +++ b/osu.Game/Beatmaps/Drawables/Cards/HoverHandlingContainer.cs @@ -0,0 +1,29 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using System; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; + +namespace osu.Game.Beatmaps.Drawables.Cards +{ + public class HoverHandlingContainer : Container + { + public Func? Hovered { get; set; } + public Action? Unhovered { get; set; } + + protected override bool OnHover(HoverEvent e) + { + bool handledByBase = base.OnHover(e); + return Hovered?.Invoke(e) ?? handledByBase; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + base.OnHoverLost(e); + Unhovered?.Invoke(e); + } + } +} diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index 49f2f5c211..6b27dbf847 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -151,7 +151,8 @@ namespace osu.Game.Overlays } // spawn new children with the contained so we only clear old content at the last moment. - var content = new FillFlowContainer + // reverse ID flow is required for correct Z-ordering of the cards' expandable content (last card should be front-most). + var content = new ReverseChildIDFillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, diff --git a/osu.Game/Overlays/Profile/ProfileSection.cs b/osu.Game/Overlays/Profile/ProfileSection.cs index 6223b32814..fc6fce0d8e 100644 --- a/osu.Game/Overlays/Profile/ProfileSection.cs +++ b/osu.Game/Overlays/Profile/ProfileSection.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Online.API.Requests.Responses; @@ -22,7 +23,7 @@ namespace osu.Game.Overlays.Profile public abstract string Identifier { get; } - private readonly FillFlowContainer content; + private readonly FillFlowContainer content; private readonly Box background; private readonly Box underscore; @@ -79,7 +80,9 @@ namespace osu.Game.Overlays.Profile } } }, - content = new FillFlowContainer + // reverse ID flow is required for correct Z-ordering of the content (last item should be front-most). + // particularly important in BeatmapsSection, as it uses beatmap cards, which have expandable overhanging content. + content = new ReverseChildIDFillFlowContainer { Direction = FillDirection.Vertical, AutoSizeAxes = Axes.Y, diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs b/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs index 130ae44273..9dcbf6142d 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs +++ b/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs @@ -13,6 +13,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; +using osu.Game.Graphics.Containers; using osu.Game.Online.API.Requests.Responses; using osuTK; @@ -26,7 +27,7 @@ namespace osu.Game.Overlays.Profile.Sections protected int VisiblePages; protected int ItemsPerPage; - protected FillFlowContainer ItemsContainer { get; private set; } + protected ReverseChildIDFillFlowContainer ItemsContainer { get; private set; } private APIRequest> retrievalRequest; private CancellationTokenSource loadCancellation; @@ -48,11 +49,15 @@ namespace osu.Game.Overlays.Profile.Sections Direction = FillDirection.Vertical, Children = new Drawable[] { - ItemsContainer = new FillFlowContainer + // reverse ID flow is required for correct Z-ordering of the items (last item should be front-most). + // particularly important in PaginatedBeatmapContainer, as it uses beatmap cards, which have expandable overhanging content. + ItemsContainer = new ReverseChildIDFillFlowContainer { AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, Spacing = new Vector2(0, 2), + // ensure the container and its contents are in front of the "more" button. + Depth = float.MinValue }, moreButton = new ShowMoreButton { diff --git a/osu.Game/Overlays/Rankings/SpotlightsLayout.cs b/osu.Game/Overlays/Rankings/SpotlightsLayout.cs index a37f762532..bcfc2499b9 100644 --- a/osu.Game/Overlays/Rankings/SpotlightsLayout.cs +++ b/osu.Game/Overlays/Rankings/SpotlightsLayout.cs @@ -135,7 +135,8 @@ namespace osu.Game.Overlays.Rankings Children = new Drawable[] { new ScoresTable(1, response.Users), - new FillFlowContainer + // reverse ID flow is required for correct Z-ordering of the cards' expandable content (last card should be front-most). + new ReverseChildIDFillFlowContainer { AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, diff --git a/osu.Game/Screens/Play/SoloSpectator.cs b/osu.Game/Screens/Play/SoloSpectator.cs index 7fea44b3ea..3918dbe8fc 100644 --- a/osu.Game/Screens/Play/SoloSpectator.cs +++ b/osu.Game/Screens/Play/SoloSpectator.cs @@ -228,7 +228,10 @@ namespace osu.Game.Screens.Play onlineBeatmapRequest.Success += beatmapSet => Schedule(() => { this.beatmapSet = beatmapSet; - beatmapPanelContainer.Child = new BeatmapCard(this.beatmapSet); + beatmapPanelContainer.Child = new BeatmapCard(this.beatmapSet) + { + Expanded = { Disabled = true } + }; checkForAutomaticDownload(); });