1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-19 23:40:44 +08:00

Merge branch 'master' into qp-fix-nullref

This commit is contained in:
Dean Herbert
2025-09-30 17:44:14 +09:00
Unverified
31 changed files with 1165 additions and 723 deletions
@@ -253,7 +253,7 @@ namespace osu.Game.Rulesets.Osu.Edit
{
var hitObjects = selectedMovableObjects;
Quad quad = GeometryUtils.GetSurroundingQuad(hitObjects);
Quad quad = GeometryUtils.GetSurroundingQuad(hitObjects, true);
Vector2 delta = Vector2.Zero;
@@ -81,12 +81,8 @@ namespace osu.Game.Rulesets.Osu.Edit
changeHandler?.BeginChange();
objectsInScale = selectedMovableObjects.ToDictionary(ho => ho, ho => new OriginalHitObjectState(ho));
OriginalSurroundingQuad = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider
? GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => slider.Position + p.Position))
: GeometryUtils.GetSurroundingQuad(objectsInScale.Keys);
originalConvexHull = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider2
? GeometryUtils.GetConvexHull(slider2.Path.ControlPoints.Select(p => slider2.Position + p.Position))
: GeometryUtils.GetConvexHull(objectsInScale.Keys);
OriginalSurroundingQuad = GeometryUtils.GetSurroundingQuad(objectsInScale.Keys);
originalConvexHull = GeometryUtils.GetConvexHull(objectsInScale.Keys);
defaultOrigin = GeometryUtils.MinimumEnclosingCircle(originalConvexHull).Item1;
}
@@ -312,7 +308,7 @@ namespace osu.Game.Rulesets.Osu.Edit
private void moveSelectionInBounds()
{
Quad quad = GeometryUtils.GetSurroundingQuad(objectsInScale!.Keys);
Quad quad = GeometryUtils.GetSurroundingQuad(objectsInScale!.Keys, true);
Vector2 delta = Vector2.Zero;
@@ -10,6 +10,7 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect;
using osu.Game.Tests.Visual.OnlinePlay;
@@ -17,11 +18,11 @@ using osuTK;
namespace osu.Game.Tests.Visual.Matchmaking
{
public partial class TestSceneSelectionGrid : OnlinePlayTestScene
public partial class TestSceneBeatmapSelectGrid : OnlinePlayTestScene
{
private MultiplayerPlaylistItem[] items = null!;
private SelectionGrid grid = null!;
private BeatmapSelectGrid grid = null!;
[Resolved]
private BeatmapManager beatmapManager { get; set; } = null!;
@@ -58,7 +59,7 @@ namespace osu.Game.Tests.Visual.Matchmaking
{
base.SetUpSteps();
AddStep("add grid", () => Child = grid = new SelectionGrid
AddStep("add grid", () => Child = grid = new BeatmapSelectGrid
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
@@ -82,6 +83,22 @@ namespace osu.Game.Tests.Visual.Matchmaking
{
// test scene is weird.
});
AddStep("add selection 1", () => grid.ChildrenOfType<BeatmapSelectPanel>().First().AddUser(new APIUser
{
Id = 6411631,
Username = "Maarvin",
}, isOwnUser: true));
AddStep("add selection 2", () => grid.ChildrenOfType<BeatmapSelectPanel>().Skip(5).First().AddUser(new APIUser
{
Id = 2,
Username = "peppy",
}));
AddStep("add selection 3", () => grid.ChildrenOfType<BeatmapSelectPanel>().Skip(10).First().AddUser(new APIUser
{
Id = 1040328,
Username = "smoogipoo",
}));
}
[Test]
@@ -154,7 +171,7 @@ namespace osu.Game.Tests.Visual.Matchmaking
var (candidateItems, _) = pickRandomItems(count);
grid.TransferCandidatePanelsToRollContainer(candidateItems);
grid.Delay(SelectionGrid.ARRANGE_DELAY)
grid.Delay(BeatmapSelectGrid.ARRANGE_DELAY)
.Schedule(() => grid.ArrangeItemsForRollAnimation());
});
@@ -162,7 +179,7 @@ namespace osu.Game.Tests.Visual.Matchmaking
AddStep("display roll order", () =>
{
var panels = grid.ChildrenOfType<SelectionPanel>().ToArray();
var panels = grid.ChildrenOfType<BeatmapSelectPanel>().ToArray();
for (int i = 0; i < panels.Length; i++)
{
@@ -12,7 +12,7 @@ using osu.Game.Tests.Visual.Multiplayer;
namespace osu.Game.Tests.Visual.Matchmaking
{
public partial class TestSceneSelectionPanel : MultiplayerTestScene
public partial class TestSceneBeatmapSelectPanel : MultiplayerTestScene
{
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
@@ -20,9 +20,9 @@ namespace osu.Game.Tests.Visual.Matchmaking
[Test]
public void TestBeatmapPanel()
{
SelectionPanel? panel = null;
BeatmapSelectPanel? panel = null;
AddStep("add panel", () => Child = panel = new SelectionPanel(new MultiplayerPlaylistItem())
AddStep("add panel", () => Child = panel = new BeatmapSelectPanel(new MultiplayerPlaylistItem())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -1,113 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.OnlinePlay.Matchmaking.Match;
using osu.Game.Tests.Visual.Multiplayer;
namespace osu.Game.Tests.Visual.Matchmaking
{
public partial class TestSceneMatchmakingScreenStack : MultiplayerTestScene
{
private const int user_count = 8;
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("join room", () =>
{
var room = CreateDefaultRoom(MatchType.Matchmaking);
room.Playlist = Enumerable.Range(1, 50).Select(i => new PlaylistItem(new MultiplayerPlaylistItem
{
ID = i,
BeatmapID = i,
StarRating = i / 10.0,
})).ToArray();
JoinRoom(room);
});
WaitForJoined();
AddStep("add carousel", () =>
{
Child = new ScreenMatchmaking.ScreenStack
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
};
});
AddStep("join users", () =>
{
var users = Enumerable.Range(1, user_count).Select(i => new MultiplayerRoomUser(i)
{
User = new APIUser
{
Username = $"Player {i}"
}
}).ToArray();
foreach (var user in users)
MultiplayerClient.AddUser(user);
});
}
[Test]
public void TestChangeStage()
{
for (int round = 1; round <= 2; round++)
{
AddLabel($"Round {round}");
int r = round;
changeStage(MatchmakingStage.RoundWarmupTime, state => state.CurrentRound = r);
changeStage(MatchmakingStage.UserBeatmapSelect);
changeStage(MatchmakingStage.ServerBeatmapFinalised, state =>
{
MultiplayerPlaylistItem[] beatmaps = Enumerable.Range(1, 8).Select(i => new MultiplayerPlaylistItem
{
ID = i,
BeatmapID = i,
StarRating = i / 10.0,
}).ToArray();
state.CandidateItems = beatmaps.Select(b => b.ID).ToArray();
state.CandidateItem = beatmaps[0].ID;
}, waitTime: 35);
changeStage(MatchmakingStage.WaitingForClientsBeatmapDownload);
changeStage(MatchmakingStage.GameplayWarmupTime);
changeStage(MatchmakingStage.Gameplay);
changeStage(MatchmakingStage.ResultsDisplaying);
}
changeStage(MatchmakingStage.Ended, state =>
{
int localUserId = API.LocalUser.Value.OnlineID;
state.Users[localUserId].Placement = 1;
state.Users[localUserId].Rounds[1].Placement = 1;
state.Users[localUserId].Rounds[1].TotalScore = 1;
state.Users[localUserId].Rounds[1].Statistics[HitResult.LargeBonus] = 1;
});
}
private void changeStage(MatchmakingStage stage, Action<MatchmakingRoomState>? prepare = null, int waitTime = 5)
{
AddStep($"stage: {stage}", () => MultiplayerClient.MatchmakingChangeStage(stage, prepare).WaitSafely());
AddWaitStep("wait", waitTime);
}
}
}
@@ -16,7 +16,7 @@ namespace osu.Game.Tests.Visual.Matchmaking
{
public partial class TestScenePlayerPanel : MultiplayerTestScene
{
private MatchmakingUserPanel panel = null!;
private PlayerPanel panel = null!;
public override void SetUpSteps()
{
@@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Matchmaking
AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.Matchmaking)));
WaitForJoined();
AddStep("add panel", () => Child = panel = new MatchmakingUserPanel(new MultiplayerRoomUser(1)
AddStep("add panel", () => Child = panel = new PlayerPanel(new MultiplayerRoomUser(1)
{
User = new APIUser
{
@@ -14,13 +14,14 @@ using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Matchmaking.Match;
using osu.Game.Tests.Visual.Multiplayer;
using osu.Game.Users;
using osuTK;
namespace osu.Game.Tests.Visual.Matchmaking
{
public partial class TestSceneUserPanelOverlay : MultiplayerTestScene
{
private UserPanelOverlay list = null!;
private PlayerPanelOverlay list = null!;
public override void SetUpSteps()
{
@@ -35,7 +36,7 @@ namespace osu.Game.Tests.Visual.Matchmaking
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.8f),
Child = list = new UserPanelOverlay()
Child = list = new PlayerPanelOverlay()
});
}
@@ -117,10 +118,10 @@ namespace osu.Game.Tests.Visual.Matchmaking
});
});
AddUntilStep("two panels displayed", () => this.ChildrenOfType<MatchmakingUserPanel>().Count(), () => Is.EqualTo(2));
AddUntilStep("two panels displayed", () => this.ChildrenOfType<UserPanel>().Count(), () => Is.EqualTo(2));
AddStep("remove a user", () => MultiplayerClient.RemoveUser(new APIUser { Id = 1 }));
AddUntilStep("one panel displayed", () => this.ChildrenOfType<MatchmakingUserPanel>().Count(), () => Is.EqualTo(1));
AddUntilStep("one panel displayed", () => this.ChildrenOfType<UserPanel>().Count(), () => Is.EqualTo(1));
}
[Test]
@@ -9,6 +9,7 @@ using osu.Game.Beatmaps;
using osu.Game.Overlays;
using osu.Game.Graphics.Carousel;
using osu.Game.Graphics.Cursor;
using osu.Game.Scoring;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Visual.UserInterface;
using osuTK;
@@ -86,6 +87,64 @@ namespace osu.Game.Tests.Visual.SongSelectV2
}
}
[Test]
public void TestRanks()
{
for (int i = -1; i <= 7; i++)
{
ScoreRank rank = (ScoreRank)i;
AddStep($"display rank {rank}", () =>
{
ContentContainer.Child = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = new (Type, object)[]
{
(typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Aquamarine))
},
Child = new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Both,
Child = new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.5f,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0f, 5f),
Children = new[]
{
new PanelGroupRankDisplay
{
Item = new CarouselItem(new RankDisplayGroupDefinition(rank))
},
new PanelGroupRankDisplay
{
Item = new CarouselItem(new RankDisplayGroupDefinition(rank)),
KeyboardSelected = { Value = true },
},
new PanelGroupRankDisplay
{
Item = new CarouselItem(new RankDisplayGroupDefinition(rank)),
Expanded = { Value = true },
},
new PanelGroupRankDisplay
{
Item = new CarouselItem(new RankDisplayGroupDefinition(rank)),
Expanded = { Value = true },
KeyboardSelected = { Value = true },
},
},
}
}
};
});
}
}
protected override Drawable CreateContent()
{
return new OsuContextMenuContainer
@@ -24,7 +24,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
public const float TRANSITION_DURATION = 340;
public const float CORNER_RADIUS = 8;
protected const float WIDTH = 345;
public const float WIDTH = 345;
public IBindable<bool> Expanded { get; }
@@ -77,25 +77,27 @@ namespace osu.Game.Beatmaps.Drawables.Cards
containingInputManager = GetContainingInputManager();
Action = () =>
{
if (containingInputManager?.CurrentState.Keyboard.ShiftPressed == true)
{
switch (DownloadTracker.State.Value)
{
case DownloadState.NotDownloaded:
if (!BeatmapSet.Availability.DownloadDisabled)
beatmaps?.Download(BeatmapSet, preferNoVideo.Value);
break;
if (Action == null)
throw new InvalidOperationException($"An action should be assigned to this {nameof(BeatmapCard)}. To use the default, assign {nameof(DefaultAction)}.");
}
case DownloadState.LocallyAvailable:
game?.PresentBeatmap(BeatmapSet);
break;
}
protected void DefaultAction()
{
if (containingInputManager?.CurrentState.Keyboard.ShiftPressed == true)
{
switch (DownloadTracker.State.Value)
{
case DownloadState.NotDownloaded:
if (!BeatmapSet.Availability.DownloadDisabled) beatmaps?.Download(BeatmapSet, preferNoVideo.Value);
break;
case DownloadState.LocallyAvailable:
game?.PresentBeatmap(BeatmapSet);
break;
}
else
beatmapSetOverlay?.FetchAndShowBeatmapSet(BeatmapSet.OnlineID);
};
}
else
beatmapSetOverlay?.FetchAndShowBeatmapSet(BeatmapSet.OnlineID);
}
protected override bool OnHover(HoverEvent e)
@@ -21,7 +21,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
public BeatmapCardContentBackground(IBeatmapSetOnlineInfo onlineInfo)
public BeatmapCardContentBackground(IBeatmapSetOnlineInfo onlineInfo, bool keepLoaded = false)
{
InternalChildren = new Drawable[]
{
@@ -29,7 +29,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{
RelativeSizeAxes = Axes.Both,
},
cover = new DelayedLoadUnloadWrapper(() => createCover(onlineInfo), 500, 500)
cover = new DelayedLoadUnloadWrapper(() => createCover(onlineInfo), keepLoaded ? 0 : 500, keepLoaded ? double.MaxValue : 1000)
{
RelativeSizeAxes = Axes.Both,
Colour = Colour4.Transparent
@@ -46,6 +46,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards
: base(beatmapSet, allowExpansion)
{
content = new BeatmapCardContent(height);
Action = DefaultAction;
}
[BackgroundDependencyLoader(true)]
@@ -54,6 +54,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards
: base(beatmapSet, false)
{
content = new BeatmapCardContent(height);
Action = DefaultAction;
}
[BackgroundDependencyLoader]
@@ -47,6 +47,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards
: base(beatmapSet, allowExpansion)
{
content = new BeatmapCardContent(HEIGHT);
Action = DefaultAction;
}
[BackgroundDependencyLoader]
@@ -35,11 +35,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
public BeatmapCardThumbnail(IBeatmapSetInfo beatmapSetInfo, IBeatmapSetOnlineInfo onlineInfo)
public BeatmapCardThumbnail(IBeatmapSetInfo beatmapSetInfo, IBeatmapSetOnlineInfo onlineInfo, bool keepLoaded = false)
{
InternalChildren = new Drawable[]
{
new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.List)
new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.List, keepLoaded ? 0 : 500, keepLoaded ? double.MaxValue : 1000)
{
RelativeSizeAxes = Axes.Both,
OnlineInfo = onlineInfo
@@ -16,10 +16,12 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons
private readonly Bindable<DownloadState> state = new Bindable<DownloadState>();
private readonly APIBeatmapSet beatmapSet;
private readonly bool allowNavigationToBeatmap;
public GoToBeatmapButton(APIBeatmapSet beatmapSet)
public GoToBeatmapButton(APIBeatmapSet beatmapSet, bool allowNavigationToBeatmap)
{
this.beatmapSet = beatmapSet;
this.allowNavigationToBeatmap = allowNavigationToBeatmap;
}
[BackgroundDependencyLoader(true)]
@@ -27,7 +29,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons
{
Action = () => game?.PresentBeatmap(beatmapSet);
Icon.Icon = FontAwesome.Solid.AngleDoubleRight;
TooltipText = "Go to beatmap";
}
protected override void LoadComplete()
@@ -40,8 +41,31 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons
private void updateState()
{
Enabled.Value = state.Value == DownloadState.LocallyAvailable;
this.FadeTo(Enabled.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
bool available = state.Value == DownloadState.LocallyAvailable;
Enabled.Value = allowNavigationToBeatmap && available;
float alpha;
if (available && allowNavigationToBeatmap)
{
TooltipText = "Go to beatmap";
Enabled.Value = true;
alpha = 1f;
}
else if (available)
{
TooltipText = string.Empty;
Enabled.Value = false;
alpha = 0.3f;
}
else
{
TooltipText = string.Empty;
Enabled.Value = false;
alpha = 0;
}
this.FadeTo(alpha, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
}
}
}
@@ -30,7 +30,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards
set
{
buttonsExpandedWidth = value;
buttonArea.Width = value;
if (IsLoaded)
updateState();
}
@@ -67,7 +66,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
public CollapsibleButtonContainer(APIBeatmapSet beatmapSet)
public CollapsibleButtonContainer(APIBeatmapSet beatmapSet, bool allowNavigationToBeatmap = true, bool keepBackgroundLoaded = false)
{
downloadTracker = new BeatmapDownloadTracker(beatmapSet);
@@ -116,14 +115,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards
RelativeSizeAxes = Axes.Both,
Height = 0.5f,
},
new GoToBeatmapButton(beatmapSet)
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
State = { BindTarget = downloadTracker.State },
RelativeSizeAxes = Axes.Both,
Height = 0.5f,
}
}
}
},
@@ -135,7 +126,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
Masking = true,
Children = new Drawable[]
{
new BeatmapCardContentBackground(beatmapSet)
new BeatmapCardContentBackground(beatmapSet, keepBackgroundLoaded)
{
RelativeSizeAxes = Axes.Both,
Dimmed = { BindTarget = ShowDetails }
@@ -152,6 +143,15 @@ namespace osu.Game.Beatmaps.Drawables.Cards
}
}
};
buttons.Add(new GoToBeatmapButton(beatmapSet, allowNavigationToBeatmap)
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
State = { BindTarget = downloadTracker.State },
RelativeSizeAxes = Axes.Both,
Height = 0.5f,
});
}
protected override void LoadComplete()
@@ -165,6 +165,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards
private void updateState()
{
buttonArea.Width = buttonsExpandedWidth;
float buttonAreaWidth = ShowDetails.Value ? ButtonsExpandedWidth : ButtonsCollapsedWidth;
float mainAreaWidth = Width - buttonAreaWidth;
@@ -0,0 +1,354 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Beatmaps.Drawables.Cards.Statistics;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapSet;
using osu.Game.Resources.Localisation.Web;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
{
public partial class BeatmapCardMatchmaking : BeatmapCard
{
private readonly APIBeatmap beatmap;
protected override Drawable IdleContent => idleBottomContent;
protected override Drawable DownloadInProgressContent => downloadProgressBar;
public const float HEIGHT = 80;
[Cached]
private readonly BeatmapCardContent content;
private BeatmapCardThumbnail thumbnail = null!;
private CollapsibleButtonContainer buttonContainer = null!;
private FillFlowContainer<BeatmapCardStatistic> statisticsContainer = null!;
private FillFlowContainer idleBottomContent = null!;
private BeatmapCardDownloadProgressBar downloadProgressBar = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
public BeatmapCardMatchmaking(APIBeatmap beatmap)
: base(beatmap.BeatmapSet!, false)
{
this.beatmap = beatmap;
content = new BeatmapCardContent(HEIGHT);
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Width = WIDTH;
Height = HEIGHT;
FillFlowContainer leftIconArea = null!;
FillFlowContainer titleBadgeArea = null!;
GridContainer artistContainer = null!;
Child = content.With(c =>
{
c.MainContent = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
thumbnail = new BeatmapCardThumbnail(BeatmapSet, BeatmapSet, keepLoaded: true)
{
Name = @"Left (icon) area",
Size = new Vector2(HEIGHT),
Padding = new MarginPadding { Right = CORNER_RADIUS },
Child = leftIconArea = new FillFlowContainer
{
Margin = new MarginPadding(4),
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(1)
}
},
buttonContainer = new CollapsibleButtonContainer(BeatmapSet, allowNavigationToBeatmap: false, keepBackgroundLoaded: true)
{
X = HEIGHT - CORNER_RADIUS,
Width = WIDTH - HEIGHT + CORNER_RADIUS,
FavouriteState = { BindTarget = FavouriteState },
ButtonsCollapsedWidth = 0,
ButtonsExpandedWidth = 24,
Children = new Drawable[]
{
new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
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 Drawable[]
{
new TruncatingSpriteText
{
Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title),
Font = OsuFont.Default.With(size: 18f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X,
},
titleBadgeArea = new FillFlowContainer
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
}
}
}
},
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 TruncatingSpriteText
{
Text = createArtistText(),
Font = OsuFont.Default.With(size: 14f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X,
},
Empty()
},
}
},
new LinkFlowContainer(s =>
{
s.Shadow = false;
s.Font = OsuFont.GetFont(size: 11f, weight: FontWeight.SemiBold);
}).With(d =>
{
d.AutoSizeAxes = Axes.Both;
d.Margin = new MarginPadding { Top = 1 };
d.AddText("mapped by ", t => t.Colour = colourProvider.Content2);
d.AddUserLink(BeatmapSet.Author);
}),
}
},
new Container
{
Name = @"Bottom content",
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Children = new Drawable[]
{
idleBottomContent = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 2),
AlwaysPresent = true,
Children = new Drawable[]
{
statisticsContainer = new FillFlowContainer<BeatmapCardStatistic>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(8, 0),
Alpha = 0,
AlwaysPresent = true,
ChildrenEnumerable = createStatistics()
},
new Container
{
Masking = true,
CornerRadius = CORNER_RADIUS,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
new Box
{
Colour = colours.ForStarDifficulty(beatmap.StarRating).Darken(0.8f),
RelativeSizeAxes = Axes.Both,
},
new FillFlowContainer
{
Padding = new MarginPadding(2),
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(4, 0),
Children = new Drawable[]
{
new StarRatingDisplay(new StarDifficulty(beatmap.StarRating, 0), StarRatingDisplaySize.Small, animated: true)
{
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Scale = new Vector2(0.875f),
},
new TruncatingSpriteText
{
Text = beatmap.DifficultyName,
Font = OsuFont.Style.Caption2.With(weight: FontWeight.Bold),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
}
}
},
}
},
}
},
downloadProgressBar = new BeatmapCardDownloadProgressBar
{
RelativeSizeAxes = Axes.X,
Height = 5,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
State = { BindTarget = DownloadTracker.State },
Progress = { BindTarget = DownloadTracker.Progress }
}
}
}
}
}
}
};
c.Expanded.BindTarget = Expanded;
});
if (BeatmapSet.HasVideo)
leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(16) });
if (BeatmapSet.HasStoryboard)
leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(16) });
if (BeatmapSet.FeaturedInSpotlight)
{
titleBadgeArea.Add(new SpotlightBeatmapBadge
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Margin = new MarginPadding { Left = 4 }
});
}
if (BeatmapSet.HasExplicitContent)
{
titleBadgeArea.Add(new ExplicitContentBeatmapBadge
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Margin = new MarginPadding { Left = 4 }
});
}
if (BeatmapSet.TrackId != null)
{
artistContainer.Content[0][1] = new FeaturedArtistBeatmapBadge
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Margin = new MarginPadding { Left = 4 }
};
}
}
private LocalisableString createArtistText()
{
var romanisableArtist = new RomanisableString(BeatmapSet.ArtistUnicode, BeatmapSet.Artist);
return BeatmapsetsStrings.ShowDetailsByArtist(romanisableArtist);
}
private IEnumerable<BeatmapCardStatistic> createStatistics()
{
var hypesStatistic = HypesStatistic.CreateFor(BeatmapSet);
if (hypesStatistic != null)
yield return hypesStatistic;
var nominationsStatistic = NominationsStatistic.CreateFor(BeatmapSet);
if (nominationsStatistic != null)
yield return nominationsStatistic;
yield return new FavouritesStatistic(BeatmapSet) { Current = FavouriteState };
yield return new PlayCountStatistic(BeatmapSet);
var dateStatistic = BeatmapCardDateStatistic.CreateFor(BeatmapSet);
if (dateStatistic != null)
yield return dateStatistic;
}
protected override void UpdateState()
{
base.UpdateState();
bool showDetails = IsHovered;
buttonContainer.ShowDetails.Value = showDetails;
thumbnail.Dimmed.Value = showDetails;
statisticsContainer.FadeTo(showDetails ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint);
}
public override MenuItem[] ContextMenuItems
{
get
{
var items = base.ContextMenuItems.ToList();
foreach (var button in buttonContainer.Buttons)
{
if (button.Enabled.Value)
items.Add(new OsuMenuItem(button.TooltipText.ToSentence(), MenuItemType.Standard, () => button.TriggerClick()));
}
return items.ToArray();
}
}
}
}
@@ -22,7 +22,7 @@ using osuTK;
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
{
public partial class SelectionGrid : CompositeDrawable
public partial class BeatmapSelectGrid : CompositeDrawable
{
public const double ARRANGE_DELAY = 200;
@@ -30,17 +30,17 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
private const double arrange_duration = 1000;
private const double roll_duration = 4000;
private const double present_beatmap_delay = 1200;
private const float panel_spacing = 20;
private const float panel_spacing = 4;
public event Action<MultiplayerPlaylistItem>? ItemSelected;
[Resolved]
private IAPIProvider api { get; set; } = null!;
private readonly Dictionary<long, SelectionPanel> panelLookup = new Dictionary<long, SelectionPanel>();
private readonly Dictionary<long, BeatmapSelectPanel> panelLookup = new Dictionary<long, BeatmapSelectPanel>();
private readonly PanelGridContainer panelGridContainer;
private readonly Container<SelectionPanel> rollContainer;
private readonly Container<BeatmapSelectPanel> rollContainer;
private readonly OsuScrollContainer scroll;
private bool allowSelection = true;
@@ -51,7 +51,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
private Sample? swooshSample;
private double? lastSamplePlayback;
public SelectionGrid()
public BeatmapSelectGrid()
{
InternalChildren = new Drawable[]
{
@@ -67,7 +67,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
Spacing = new Vector2(panel_spacing)
},
},
rollContainer = new Container<SelectionPanel>
rollContainer = new Container<BeatmapSelectPanel>
{
RelativeSizeAxes = Axes.Both,
Masking = true,
@@ -108,9 +108,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
public void AddItem(MultiplayerPlaylistItem item)
{
var panel = panelLookup[item.ID] = new SelectionPanel(item)
var panel = panelLookup[item.ID] = new BeatmapSelectPanel(item)
{
Size = new Vector2(300, 70),
AllowSelection = allowSelection,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
@@ -176,7 +175,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
var rng = new Random();
var remainingPanels = new List<SelectionPanel>();
var remainingPanels = new List<BeatmapSelectPanel>();
foreach (var panel in panelGridContainer.Children.ToArray())
{
@@ -216,7 +215,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
{
var panel = rollContainer.Children[i];
var position = positions[i] * (SelectionPanel.SIZE + new Vector2(panel_spacing));
var position = positions[i] * (BeatmapSelectPanel.SIZE + new Vector2(panel_spacing));
panel.MoveTo(position, duration + stagger * i, new SplitEasingFunction(Easing.InCubic, Easing.OutExpo, 0.3f));
@@ -285,7 +284,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
while ((numSteps - 1) % rollContainer.Children.Count != finalItemIndex)
numSteps++;
SelectionPanel? lastPanel = null;
BeatmapSelectPanel? lastPanel = null;
for (int i = 0; i < numSteps; i++)
{
@@ -330,7 +329,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
{
rollContainer.ChangeChildDepth(panel, float.MinValue);
panel.ShowBorder();
panel.ShowChosenBorder();
panel.MoveTo(Vector2.Zero, 1000, Easing.OutExpo)
.ScaleTo(1.5f, 1000, Easing.OutExpo);
@@ -346,7 +345,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
PresentRolledBeatmap(finalItem);
}
private partial class PanelGridContainer : FillFlowContainer<SelectionPanel>
private partial class PanelGridContainer : FillFlowContainer<BeatmapSelectPanel>
{
public bool LayoutDisabled;
@@ -0,0 +1,341 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions;
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.Game.Beatmaps.Drawables.Cards;
using osu.Game.Database;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
{
public partial class BeatmapSelectPanel : Container
{
public static readonly Vector2 SIZE = new Vector2(BeatmapCard.WIDTH, BeatmapCardNormal.HEIGHT);
public bool AllowSelection { get; set; }
public readonly MultiplayerPlaylistItem Item;
public Action<MultiplayerPlaylistItem>? Action { private get; init; }
private const float border_width = 3;
private Container scaleContainer = null!;
private AvatarOverlay selectionOverlay = null!;
private Drawable lighting = null!;
private Container border = null!;
private Container mainContent = null!;
public override bool PropagatePositionalInputSubTree => AllowSelection;
public BeatmapSelectPanel(MultiplayerPlaylistItem item)
{
Item = item;
Size = SIZE;
}
[BackgroundDependencyLoader]
private void load(BeatmapLookupCache lookupCache, OverlayColourProvider colourProvider)
{
InternalChild = scaleContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new[]
{
mainContent = new Container
{
Masking = true,
CornerRadius = BeatmapCard.CORNER_RADIUS,
CornerExponent = 10,
RelativeSizeAxes = Axes.Both,
Children = new[]
{
lighting = new Box
{
Blending = BlendingParameters.Additive,
RelativeSizeAxes = Axes.Both,
Alpha = 0,
},
selectionOverlay = new AvatarOverlay
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
}
}
},
border = new Container
{
Alpha = 0,
Masking = true,
CornerRadius = BeatmapCard.CORNER_RADIUS,
CornerExponent = 10,
Blending = BlendingParameters.Additive,
RelativeSizeAxes = Axes.Both,
BorderThickness = border_width,
BorderColour = colourProvider.Light1,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Radius = 40,
Roundness = 300,
Colour = colourProvider.Light3.Opacity(0.1f),
},
Children = new Drawable[]
{
new Box
{
AlwaysPresent = true,
Alpha = 0,
Colour = Color4.Black,
RelativeSizeAxes = Axes.Both,
},
}
},
}
};
lookupCache.GetBeatmapAsync(Item.BeatmapID).ContinueWith(b => Schedule(() =>
{
var beatmap = b.GetResultSafely()!;
beatmap.StarRating = Item.StarRating;
mainContent.Add(new BeatmapCardMatchmaking(beatmap)
{
Depth = float.MaxValue,
Action = () => Action?.Invoke(Item),
});
}));
}
public bool AddUser(APIUser user, bool isOwnUser = false) => selectionOverlay.AddUser(user, isOwnUser);
public bool RemoveUser(APIUser user) => selectionOverlay.RemoveUser(user.Id);
protected override bool OnHover(HoverEvent e)
{
lighting.FadeTo(0.2f, 50)
.Then()
.FadeTo(0.1f, 300);
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
base.OnHoverLost(e);
lighting.FadeOut(200);
}
protected override bool OnMouseDown(MouseDownEvent e)
{
if (e.Button == MouseButton.Left)
{
scaleContainer.ScaleTo(0.95f, 400, Easing.OutExpo);
return true;
}
return base.OnMouseDown(e);
}
protected override void OnMouseUp(MouseUpEvent e)
{
base.OnMouseUp(e);
if (e.Button == MouseButton.Left)
{
scaleContainer.ScaleTo(1f, 500, Easing.OutElasticHalf);
}
}
protected override bool OnClick(ClickEvent e)
{
lighting.FadeTo(0.5f, 50)
.Then()
.FadeTo(0.1f, 400);
// pass through to let the beatmap card handle actual click.
return false;
}
public void ShowChosenBorder()
{
border.FadeTo(1, 1000, Easing.OutQuint);
}
public void ShowBorder()
{
border.FadeTo(1, 80, Easing.OutQuint)
.Then()
.FadeTo(0.7f, 800, Easing.OutQuint);
}
public void HideBorder()
{
border.FadeOut(500, Easing.OutQuint);
}
public void FadeInAndEnterFromBelow(double duration = 500, double delay = 0, float distance = 200)
{
scaleContainer
.FadeOut()
.MoveToY(distance)
.Delay(delay)
.FadeIn(duration / 2)
.MoveToY(0, duration, Easing.OutExpo);
}
public void PopOutAndExpire(double duration = 400, double delay = 0, Easing easing = Easing.InCubic)
{
AllowSelection = false;
scaleContainer.Delay(delay)
.ScaleTo(0, duration, easing)
.FadeOut(duration);
this.Delay(delay + duration).FadeOut().Expire();
}
private partial class AvatarOverlay : CompositeDrawable
{
private readonly Container<SelectionAvatar> avatars;
private Sample? userAddedSample;
private double? lastSamplePlayback;
public AvatarOverlay()
{
AutoSizeAxes = Axes.Both;
InternalChild = avatars = new Container<SelectionAvatar>
{
AutoSizeAxes = Axes.X,
Height = SelectionAvatar.AVATAR_SIZE,
};
Padding = new MarginPadding(5);
}
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
userAddedSample = audio.Samples.Get(@"Multiplayer/player-ready");
}
public bool AddUser(APIUser user, bool isOwnUser)
{
if (avatars.Any(a => a.User.Id == user.Id))
return false;
var avatar = new SelectionAvatar(user, isOwnUser);
avatars.Add(avatar);
if (lastSamplePlayback == null || Time.Current - lastSamplePlayback > OsuGameBase.SAMPLE_DEBOUNCE_TIME)
{
userAddedSample?.Play();
lastSamplePlayback = Time.Current;
}
updateAvatarLayout();
avatar.FinishTransforms();
return true;
}
public bool RemoveUser(int id)
{
if (avatars.SingleOrDefault(a => a.User.Id == id) is not SelectionAvatar avatar)
return false;
avatar.PopOutAndExpire();
avatars.ChangeChildDepth(avatar, float.MaxValue);
updateAvatarLayout();
return true;
}
private void updateAvatarLayout()
{
const double stagger = 30;
const float spacing = 4;
double delay = 0;
float x = 0;
for (int i = avatars.Count - 1; i >= 0; i--)
{
var avatar = avatars[i];
if (avatar.Expired)
continue;
avatar.Delay(delay).MoveToX(x, 500, Easing.OutElasticQuarter);
x -= avatar.LayoutSize.X + spacing;
delay += stagger;
}
}
public partial class SelectionAvatar : CompositeDrawable
{
public const float AVATAR_SIZE = 30;
public APIUser User { get; }
public bool Expired { get; private set; }
private readonly MatchmakingAvatar avatar;
public SelectionAvatar(APIUser user, bool isOwnUser)
{
User = user;
Size = new Vector2(AVATAR_SIZE);
InternalChild = avatar = new MatchmakingAvatar(user, isOwnUser)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};
}
protected override void LoadComplete()
{
base.LoadComplete();
avatar.ScaleTo(0)
.ScaleTo(1, 500, Easing.OutElasticHalf)
.FadeIn(200);
}
public void PopOutAndExpire()
{
avatar.ScaleTo(0, 400, Easing.OutExpo);
this.FadeOut(100).Expire();
Expired = true;
}
}
}
}
}
@@ -1,502 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osuTK;
using osuTK.Input;
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
{
public partial class SelectionPanel : Container
{
public static readonly Vector2 SIZE = new Vector2(300, 70);
private const float corner_radius = 6;
private const float border_width = 3;
public readonly MultiplayerPlaylistItem Item;
private readonly Container scaleContainer;
private readonly BeatmapPanel beatmapPanel;
private readonly AvatarOverlay selectionOverlay;
private readonly Container border;
private readonly Box flash;
public bool AllowSelection;
public Action<MultiplayerPlaylistItem>? Action;
[Resolved]
private BeatmapLookupCache beatmapLookupCache { get; set; } = null!;
public override bool PropagatePositionalInputSubTree => AllowSelection;
public SelectionPanel(MultiplayerPlaylistItem item)
{
Item = item;
Size = SIZE;
InternalChildren = new Drawable[]
{
scaleContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(-border_width),
Child = border = new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = corner_radius + border_width,
Alpha = 0,
Child = new Box { RelativeSizeAxes = Axes.Both },
}
},
beatmapPanel = new BeatmapPanel
{
RelativeSizeAxes = Axes.Both,
OverlayLayer =
{
Children = new[]
{
flash = new Box
{
Blending = BlendingParameters.Additive,
RelativeSizeAxes = Axes.Both,
Alpha = 0,
},
}
}
},
selectionOverlay = new AvatarOverlay
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = 10 },
Origin = Anchor.CentreLeft,
},
}
},
new HoverClickSounds(),
};
}
protected override void LoadComplete()
{
base.LoadComplete();
beatmapLookupCache.GetBeatmapAsync(Item.BeatmapID).ContinueWith(b => Schedule(() =>
{
var beatmap = b.GetResultSafely()!;
beatmap.StarRating = Item.StarRating;
beatmapPanel.Beatmap = beatmap;
}));
}
public bool AddUser(APIUser user, bool isOwnUser = false) => selectionOverlay.AddUser(user, isOwnUser);
public bool RemoveUser(int userId) => selectionOverlay.RemoveUser(userId);
public bool RemoveUser(APIUser user) => RemoveUser(user.Id);
protected override bool OnHover(HoverEvent e)
{
flash.FadeTo(0.2f, 50)
.Then()
.FadeTo(0.1f, 300);
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
base.OnHoverLost(e);
flash.FadeOut(200);
}
protected override bool OnMouseDown(MouseDownEvent e)
{
if (e.Button == MouseButton.Left)
{
scaleContainer.ScaleTo(0.95f, 400, Easing.OutExpo);
return true;
}
return base.OnMouseDown(e);
}
protected override void OnMouseUp(MouseUpEvent e)
{
base.OnMouseUp(e);
if (e.Button == MouseButton.Left)
{
scaleContainer.ScaleTo(1f, 500, Easing.OutElasticHalf);
}
}
protected override bool OnClick(ClickEvent e)
{
Action?.Invoke(Item);
flash.FadeTo(0.5f, 50)
.Then()
.FadeTo(0.1f, 400);
return true;
}
public void ShowBorder() => border.Show();
public void HideBorder() => border.Hide();
public void FadeInAndEnterFromBelow(double duration = 500, double delay = 0, float distance = 200)
{
scaleContainer
.FadeOut()
.MoveToY(distance)
.Delay(delay)
.FadeIn(duration / 2)
.MoveToY(0, duration, Easing.OutExpo);
}
public void PopOutAndExpire(double duration = 400, double delay = 0, Easing easing = Easing.InCubic)
{
AllowSelection = false;
scaleContainer.Delay(delay)
.ScaleTo(0, duration, easing)
.FadeOut(duration);
this.Delay(delay + duration).FadeOut().Expire();
}
// TODO: combine following two classes with above implementation for simplicity?
private partial class BeatmapPanel : CompositeDrawable
{
public readonly Container OverlayLayer = new Container { RelativeSizeAxes = Axes.Both };
public APIBeatmap? Beatmap
{
get => beatmap;
set
{
if (beatmap?.OnlineID == value?.OnlineID)
return;
beatmap = value;
if (IsLoaded)
updateContent();
}
}
private APIBeatmap? beatmap;
private Container content = null!;
private UpdateableOnlineBeatmapSetCover cover = null!;
public BeatmapPanel(APIBeatmap? beatmap = null)
{
this.beatmap = beatmap;
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
Masking = true;
CornerRadius = 6;
InternalChildren = new Drawable[]
{
cover = new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.Card, timeBeforeLoad: 0, timeBeforeUnload: 10000)
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientHorizontal(
colourProvider.Background4.Opacity(0.7f),
colourProvider.Background4.Opacity(0.4f)
)
},
content = new Container
{
RelativeSizeAxes = Axes.Both,
},
OverlayLayer,
};
}
protected override void LoadComplete()
{
base.LoadComplete();
updateContent();
FinishTransforms(true);
}
private void updateContent()
{
foreach (var child in content.Children)
child.FadeOut(300).Expire();
cover.OnlineInfo = beatmap?.BeatmapSet;
if (beatmap != null)
{
var panelContent = new BeatmapPanelContent(beatmap)
{
RelativeSizeAxes = Axes.Both,
};
content.Add(panelContent);
panelContent.FadeInFromZero(300);
}
}
private partial class BeatmapPanelContent : CompositeDrawable
{
private readonly APIBeatmap beatmap;
public BeatmapPanelContent(APIBeatmap beatmap)
{
this.beatmap = beatmap;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChild = new FillFlowContainer
{
Direction = FillDirection.Vertical,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Padding = new MarginPadding { Horizontal = 12 },
Children = new Drawable[]
{
new TruncatingSpriteText
{
Text = new RomanisableString(beatmap.Metadata.TitleUnicode, beatmap.Metadata.TitleUnicode),
Font = OsuFont.Default.With(size: 19, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X,
},
new TextFlowContainer(s =>
{
s.Font = OsuFont.GetFont(size: 16, weight: FontWeight.SemiBold);
}).With(d =>
{
d.RelativeSizeAxes = Axes.X;
d.AutoSizeAxes = Axes.Y;
d.AddText("by ");
d.AddText(new RomanisableString(beatmap.Metadata.ArtistUnicode, beatmap.Metadata.Artist));
}),
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Margin = new MarginPadding { Top = 6 },
Spacing = new Vector2(4),
Children = new Drawable[]
{
new StarRatingDisplay(new StarDifficulty(beatmap.StarRating, 0), StarRatingDisplaySize.Small)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
new TruncatingSpriteText
{
Text = beatmap.DifficultyName,
Font = OsuFont.Default.With(size: 16, weight: FontWeight.SemiBold),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
}
},
},
};
}
}
}
private partial class AvatarOverlay : CompositeDrawable
{
private readonly Dictionary<int, SelectionAvatar> avatars = new Dictionary<int, SelectionAvatar>();
private readonly Container<SelectionAvatar> avatarContainer;
private Sample? userAddedSample;
private double? lastSamplePlayback;
public new Axes AutoSizeAxes
{
get => base.AutoSizeAxes;
set => base.AutoSizeAxes = value;
}
public new MarginPadding Padding
{
get => base.Padding;
set => base.Padding = value;
}
public AvatarOverlay()
{
InternalChild = avatarContainer = new Container<SelectionAvatar>();
}
protected override void LoadComplete()
{
base.LoadComplete();
avatarContainer.AutoSizeAxes = AutoSizeAxes;
avatarContainer.RelativeSizeAxes = RelativeSizeAxes;
}
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
userAddedSample = audio.Samples.Get(@"Multiplayer/player-ready");
}
public bool AddUser(APIUser user, bool isOwnUser)
{
if (avatars.ContainsKey(user.Id))
return false;
var avatar = new SelectionAvatar(user, isOwnUser)
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
};
avatarContainer.Add(avatars[user.Id] = avatar);
if (lastSamplePlayback == null || Time.Current - lastSamplePlayback > OsuGameBase.SAMPLE_DEBOUNCE_TIME)
{
userAddedSample?.Play();
lastSamplePlayback = Time.Current;
}
updateLayout();
avatar.FinishTransforms();
return true;
}
public bool RemoveUser(int id)
{
if (!avatars.Remove(id, out var avatar))
return false;
avatar.PopOutAndExpire();
avatarContainer.ChangeChildDepth(avatar, float.MaxValue);
updateLayout();
return true;
}
private void updateLayout()
{
const double stagger = 30;
const float spacing = 4;
double delay = 0;
float x = 0;
for (int i = avatarContainer.Count - 1; i >= 0; i--)
{
var avatar = avatarContainer[i];
if (avatar.Expired)
continue;
avatar.Delay(delay).MoveToX(x, 500, Easing.OutElasticQuarter);
x -= avatar.LayoutSize.X + spacing;
delay += stagger;
}
}
public partial class SelectionAvatar : CompositeDrawable
{
public bool Expired { get; private set; }
private readonly Container content;
public SelectionAvatar(APIUser user, bool isOwnUser)
{
Size = new Vector2(30);
InternalChildren = new Drawable[]
{
content = new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Child = new MatchmakingAvatar(user, isOwnUser)
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
content.ScaleTo(0)
.ScaleTo(1, 500, Easing.OutElasticHalf)
.FadeIn(200);
}
public void PopOutAndExpire()
{
content.ScaleTo(0, 400, Easing.OutExpo);
this.FadeOut(100).Expire();
Expired = true;
}
}
}
}
}
@@ -16,7 +16,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
public override PanelDisplayStyle PlayersDisplayStyle => PanelDisplayStyle.Split;
public override Drawable PlayersDisplayArea { get; }
private readonly SelectionGrid selectionGrid;
private readonly BeatmapSelectGrid beatmapSelectGrid;
[Resolved]
private MultiplayerClient client { get; set; } = null!;
@@ -29,7 +29,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Horizontal = 200 },
Child = selectionGrid = new SelectionGrid
Child = beatmapSelectGrid = new BeatmapSelectGrid
{
RelativeSizeAxes = Axes.Both,
},
@@ -37,8 +37,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Horizontal = 5 },
Child = PlayersDisplayArea = Empty().With(d =>
Child = PlayersDisplayArea = new Container().With(d =>
{
d.RelativeSizeAxes = Axes.Both;
})
@@ -55,7 +54,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
foreach (var item in client.Room!.Playlist)
onItemAdded(item);
selectionGrid.ItemSelected += item => client.MatchmakingToggleSelection(item.ID);
beatmapSelectGrid.ItemSelected += item => client.MatchmakingToggleSelection(item.ID);
client.MatchmakingItemSelected += onItemSelected;
client.MatchmakingItemDeselected += onItemDeselected;
@@ -66,22 +65,22 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
if (item.Expired)
return;
selectionGrid.AddItem(item);
beatmapSelectGrid.AddItem(item);
});
private void onItemSelected(int userId, long itemId)
{
var user = client.Room!.Users.First(it => it.UserID == userId).User!;
selectionGrid.SetUserSelection(user, itemId, true);
beatmapSelectGrid.SetUserSelection(user, itemId, true);
}
private void onItemDeselected(int userId, long itemId)
{
var user = client.Room!.Users.First(it => it.UserID == userId).User!;
selectionGrid.SetUserSelection(user, itemId, false);
beatmapSelectGrid.SetUserSelection(user, itemId, false);
}
public void RollFinalBeatmap(long[] candidateItems, long finalItem) => selectionGrid.RollAndDisplayFinalBeatmap(candidateItems, finalItem);
public void RollFinalBeatmap(long[] candidateItems, long finalItem) => beatmapSelectGrid.RollAndDisplayFinalBeatmap(candidateItems, finalItem);
protected override void Dispose(bool isDisposing)
{
@@ -15,7 +15,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
{
/// <summary>
/// A circular player avatar used in matchmaking displays.
/// Is part of a <see cref="MatchmakingUserPanel"/> but can also be used in isolation for a more ambient/decorative user display.
/// Is part of a <see cref="PlayerPanel"/> but can also be used in isolation for a more ambient/decorative user display.
/// </summary>
public partial class MatchmakingAvatar : CompositeDrawable
{
@@ -20,7 +20,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
/// A panel used throughout matchmaking to represent a user, including local information like their
/// rank and high level statistics in the matchmaking system.
/// </summary>
public partial class MatchmakingUserPanel : UserPanel
public partial class PlayerPanel : UserPanel
{
public static readonly Vector2 SIZE_HORIZONTAL = new Vector2(250, 100);
public static readonly Vector2 SIZE_VERTICAL = new Vector2(150, 200);
@@ -55,7 +55,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
private bool horizontal;
public MatchmakingUserPanel(MultiplayerRoomUser user)
public PlayerPanel(MultiplayerRoomUser user)
: base(user.User!)
{
RoomUser = user;
@@ -66,6 +66,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
{
Masking = true;
CornerRadius = 10;
CornerExponent = 10;
Add(scaleContainer = new Container
{
@@ -18,12 +18,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
/// A component which maintains the layout of the players in a matchmaking room.
/// Can be controlled to display the panels in a certain location and in multiple styles.
/// </summary>
public partial class UserPanelOverlay : CompositeDrawable
public partial class PlayerPanelOverlay : CompositeDrawable
{
[Resolved]
private MultiplayerClient client { get; set; } = null!;
private Container<MatchmakingUserPanel> panels = null!;
private Container<PlayerPanel> panels = null!;
private PlayerPanelCellContainer gridLayout = null!;
private PlayerPanelCellContainer splitLayoutLeft = null!;
private PlayerPanelCellContainer splitLayoutRight = null!;
@@ -40,7 +40,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
gridLayout = new PlayerPanelCellContainer
{
RelativeSizeAxes = Axes.Both,
Spacing = new Vector2(20, 5),
Spacing = new Vector2(20),
},
splitLayoutLeft = new PlayerPanelCellContainer
{
@@ -49,7 +49,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
Spacing = new Vector2(20, 5),
Spacing = new Vector2(5),
},
splitLayoutRight = new PlayerPanelCellContainer
{
@@ -58,9 +58,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
Spacing = new Vector2(20, 5),
Spacing = new Vector2(5),
},
panels = new Container<MatchmakingUserPanel>
panels = new Container<PlayerPanel>
{
RelativeSizeAxes = Axes.Both
}
@@ -110,7 +110,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
private void onUserJoined(MultiplayerRoomUser user) => Scheduler.Add(() =>
{
panels.Add(new MatchmakingUserPanel(user)
panels.Add(new PlayerPanel(user)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -215,7 +215,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
[Resolved]
private MultiplayerClient client { get; set; } = null!;
public void AcquirePanels(MatchmakingUserPanel[] panels)
public void AcquirePanels(PlayerPanel[] panels)
{
while (Count < panels.Length)
{
@@ -259,10 +259,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
private partial class PlayerPanelCell : Drawable
{
private MatchmakingUserPanel? panel;
private PlayerPanel? panel;
private bool isAnimating;
public void AcquirePanel(MatchmakingUserPanel panel)
public void AcquirePanel(PlayerPanel panel)
{
this.panel = panel;
isAnimating = true;
@@ -280,7 +280,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
if (panel?.Parent == null)
return;
Size = panel.Horizontal ? MatchmakingUserPanel.SIZE_HORIZONTAL : MatchmakingUserPanel.SIZE_VERTICAL;
Size = panel.Horizontal ? PlayerPanel.SIZE_HORIZONTAL : PlayerPanel.SIZE_VERTICAL;
Size *= panel.Scale;
var targetPos = getFinalPosition();
@@ -24,7 +24,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
private MultiplayerClient client { get; set; } = null!;
private Framework.Screens.ScreenStack screenStack = null!;
private UserPanelOverlay playersList = null!;
private PlayerPanelOverlay playersList = null!;
[BackgroundDependencyLoader]
private void load()
@@ -45,7 +45,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
screenStack = new Framework.Screens.ScreenStack(),
}
},
playersList = new UserPanelOverlay
playersList = new PlayerPanelOverlay
{
DisplayArea = this
},
@@ -42,6 +42,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
private Sample? countdownTickSample;
private double? lastSamplePlayback;
private Container mainContent = null!;
public bool Active { get; private set; }
public float Progress => progressBar.Width;
@@ -49,10 +51,14 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
public StageSegment(int? round, MatchmakingStage stage, LocalisableString displayText)
{
Round = round;
this.stage = stage;
this.displayText = displayText;
AutoSizeAxes = Axes.Both;
Anchor = Anchor.CentreLeft;
Origin = Anchor.CentreLeft;
}
[BackgroundDependencyLoader]
@@ -74,7 +80,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
Icon = FontAwesome.Solid.ArrowRight,
Margin = new MarginPadding { Horizontal = 10 }
},
new Container
mainContent = new Container
{
Masking = true,
CornerRadius = 5,
@@ -178,6 +184,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
if (wasActive)
progressBar.Width = 1;
mainContent.ScaleTo(Active ? 1.3f : 1, 500, Easing.OutQuint);
bool isPreparing =
(stage == MatchmakingStage.RoundWarmupTime && roomState.Stage == MatchmakingStage.WaitingForClientsJoin) ||
(stage == MatchmakingStage.GameplayWarmupTime && roomState.Stage == MatchmakingStage.WaitingForClientsBeatmapDownload) ||
@@ -57,17 +57,16 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
{
scroll = new StageScrollContainer
{
ScrollbarOverlapsContent = false,
ScrollbarVisible = false,
ClampExtension = 0,
RelativeSizeAxes = Axes.X,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Height = 36,
Height = HEIGHT,
Child = flow = new FillFlowContainer
{
Padding = new MarginPadding { Horizontal = 2000 },
AutoSizeAxes = Axes.Both,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Direction = FillDirection.Horizontal,
},
},
@@ -226,8 +225,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
round = value.Value;
this.ScaleTo(6, 500, Easing.OutQuart)
.MoveToY(-300, 500, Easing.OutQuart)
this.ScaleTo(6, 1000, Easing.OutPow10)
.MoveToY(-300, 1000, Easing.OutPow10)
.Then()
.MoveToY(0, 500, Easing.InQuart)
.ScaleTo(1, 500, Easing.InQuart);
@@ -13,6 +13,7 @@ using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
@@ -788,9 +789,11 @@ namespace osu.Game.Screens.SelectV2
private readonly DrawablePool<PanelBeatmapSet> setPanelPool = new DrawablePool<PanelBeatmapSet>(100);
private readonly DrawablePool<PanelGroup> groupPanelPool = new DrawablePool<PanelGroup>(100);
private readonly DrawablePool<PanelGroupStarDifficulty> starsGroupPanelPool = new DrawablePool<PanelGroupStarDifficulty>(11);
private readonly DrawablePool<PanelGroupRankDisplay> ranksGroupPanelPool = new DrawablePool<PanelGroupRankDisplay>(9);
private void setupPools()
{
AddInternal(ranksGroupPanelPool);
AddInternal(starsGroupPanelPool);
AddInternal(groupPanelPool);
AddInternal(beatmapPanelPool);
@@ -829,6 +832,9 @@ namespace osu.Game.Screens.SelectV2
case StarDifficultyGroupDefinition:
return starsGroupPanelPool.Get();
case RankDisplayGroupDefinition:
return ranksGroupPanelPool.Get();
case GroupDefinition:
return groupPanelPool.Get();
@@ -1085,6 +1091,11 @@ namespace osu.Game.Screens.SelectV2
/// </summary>
public record StarDifficultyGroupDefinition(int Order, string Title, StarDifficulty Difficulty) : GroupDefinition(Order, Title);
/// <summary>
/// Defines a grouping header for a set of carousel items grouped by achieved rank.
/// </summary>
public record RankDisplayGroupDefinition(ScoreRank Rank) : GroupDefinition(-(int)Rank, Rank.GetDescription());
/// <summary>
/// Used to represent a portion of a <see cref="BeatmapSetInfo"/> under a <see cref="GroupDefinition"/>.
/// The purpose of this model is to support splitting beatmap sets apart when the active grouping mode demands it.
@@ -433,7 +433,7 @@ namespace osu.Game.Screens.SelectV2
private IEnumerable<GroupDefinition> defineGroupByRankAchieved(BeatmapInfo beatmap, IReadOnlyDictionary<Guid, ScoreRank> topRankMapping)
{
if (topRankMapping.TryGetValue(beatmap.ID, out var rank))
return new GroupDefinition(-(int)rank, rank.GetDescription()).Yield();
return new RankDisplayGroupDefinition(rank).Yield();
return new GroupDefinition(int.MaxValue, "Unplayed").Yield();
}
@@ -0,0 +1,226 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Leaderboards;
using osu.Game.Overlays;
using osu.Game.Scoring;
using osuTK;
using osuTK.Graphics;
using WebCommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings;
namespace osu.Game.Screens.SelectV2
{
public partial class PanelGroupRankDisplay : Panel
{
public const float HEIGHT = PanelGroup.HEIGHT;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
private Drawable iconContainer = null!;
private Box backgroundBorder = null!;
private Box contentBackground = null!;
private OsuSpriteText starRatingText = null!;
private CircularContainer countPill = null!;
private OsuSpriteText countText = null!;
private TrianglesV2 triangles = null!;
private Box glow = null!;
[BackgroundDependencyLoader]
private void load()
{
Height = PanelGroup.HEIGHT;
Icon = iconContainer = new Container
{
AlwaysPresent = true,
RelativeSizeAxes = Axes.Y,
Alpha = 0f,
Child = new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Icon = FontAwesome.Solid.ChevronDown,
Size = new Vector2(12),
},
};
Background = backgroundBorder = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Highlight1,
};
AccentColour = colourProvider.Highlight1;
Content.Children = new Drawable[]
{
contentBackground = new Box
{
RelativeSizeAxes = Axes.Both,
},
triangles = new TrianglesV2
{
RelativeSizeAxes = Axes.Both,
Thickness = 0.02f,
SpawnRatio = 0.6f,
Colour = ColourInfo.GradientHorizontal(colourProvider.Background6, colourProvider.Background5)
},
glow = new Box
{
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
Colour = ColourInfo.GradientHorizontal(colourProvider.Highlight1, colourProvider.Highlight1.Opacity(0f)),
},
new FillFlowContainer
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(10f, 0f),
Margin = new MarginPadding { Left = 10f },
Children = new Drawable[]
{
starRatingText = new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
UseFullGlyphHeight = false,
Font = OsuFont.Style.Heading2,
}
}
},
countPill = new CircularContainer
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Size = new Vector2(50f, 14f),
Margin = new MarginPadding { Right = 30f },
Masking = true,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black.Opacity(0.7f),
},
countText = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.Style.Caption1.With(weight: FontWeight.Bold),
UseFullGlyphHeight = false,
}
},
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Expanded.BindValueChanged(_ => onExpanded(), true);
}
private Color4 rankColour;
protected override void PrepareForUse()
{
base.PrepareForUse();
Debug.Assert(Item != null);
var group = (RankDisplayGroupDefinition)Item.Model;
ScoreRank rank = group.Rank;
rankColour = OsuColour.ForRank(rank);
AccentColour = rankColour;
backgroundBorder.Colour = rankColour;
contentBackground.Colour = rankColour.Darken(1f);
glow.Colour = ColourInfo.GradientHorizontal(rankColour, rankColour.Opacity(0f));
switch (rank)
{
case ScoreRank.SH:
case ScoreRank.XH:
starRatingText.Colour = DrawableRank.GetRankNameColour(rank);
iconContainer.Colour = colourProvider.Background5;
break;
case ScoreRank.X:
case ScoreRank.S:
starRatingText.Colour = DrawableRank.GetRankNameColour(rank);
iconContainer.Colour = colourProvider.Background5;
break;
case ScoreRank.F:
starRatingText.Colour = DrawableRank.GetRankNameColour(rank);
iconContainer.Colour = colourProvider.Content1;
break;
default:
starRatingText.Colour = Color4.White;
iconContainer.Colour = colourProvider.Background5;
break;
}
starRatingText.Text = group.Title;
ColourInfo colour = ColourInfo.GradientHorizontal(rankColour.Darken(0.6f), rankColour.Darken(0.8f));
triangles.Colour = colour;
countText.Text = Item.NestedItemCount.ToLocalisableString(@"N0");
onExpanded();
}
private void onExpanded()
{
const float duration = 500;
iconContainer.ResizeWidthTo(Expanded.Value ? 20f : 5f, duration, Easing.OutQuint);
iconContainer.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint);
glow.FadeTo(Expanded.Value ? 0.4f : 0, duration, Easing.OutQuint);
}
protected override void Update()
{
base.Update();
// Move the count pill in the opposite direction to keep it pinned to the screen regardless of the X position of TopLevelContent.
countPill.X = -TopLevelContent.X;
}
public override MenuItem[] ContextMenuItems
{
get
{
if (Item == null)
return Array.Empty<MenuItem>();
return new MenuItem[]
{
new OsuMenuItem(Expanded.Value ? WebCommonStrings.ButtonsCollapse.ToSentence() : WebCommonStrings.ButtonsExpand.ToSentence(), MenuItemType.Highlighted, () => TriggerClick())
};
}
}
}
}
+15 -3
View File
@@ -144,8 +144,9 @@ namespace osu.Game.Utils
/// Returns a gamefield-space quad surrounding the provided hit objects.
/// </summary>
/// <param name="hitObjects">The hit objects to calculate a quad for.</param>
public static Quad GetSurroundingQuad(IEnumerable<IHasPosition> hitObjects) =>
GetSurroundingQuad(enumerateStartAndEndPositions(hitObjects));
/// <param name="startAndEndOnly">Whether to only include the start and end positions of the slider, or include every control point in the slider.</param>
public static Quad GetSurroundingQuad(IEnumerable<IHasPosition> hitObjects, bool startAndEndOnly = false) =>
GetSurroundingQuad(startAndEndOnly ? enumerateStartAndEndPositions(hitObjects) : enumeratePositions(hitObjects));
/// <summary>
/// Returns the points that make up the convex hull of the provided points.
@@ -202,7 +203,7 @@ namespace osu.Game.Utils
}
public static List<Vector2> GetConvexHull(IEnumerable<IHasPosition> hitObjects) =>
GetConvexHull(enumerateStartAndEndPositions(hitObjects));
GetConvexHull(enumeratePositions(hitObjects));
private static IEnumerable<Vector2> enumerateStartAndEndPositions(IEnumerable<IHasPosition> hitObjects) =>
hitObjects.SelectMany(h =>
@@ -220,6 +221,17 @@ namespace osu.Game.Utils
return new[] { h.Position };
});
private static IEnumerable<Vector2> enumeratePositions(IEnumerable<IHasPosition> hitObjects) =>
hitObjects.SelectMany(h =>
{
if (h is IHasPath path)
{
return path.Path.ControlPoints.Select(p => h.Position + p.Position);
}
return new[] { h.Position };
});
#region Welzl helpers
// Function to check whether a point lies inside or on the boundaries of the circle