// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. #nullable disable using System.Collections.Specialized; using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Users.Drawables; using osuTK; namespace osu.Game.Screens.OnlinePlay.Lounge.Components { public partial class DrawableRoomParticipantsList : OnlinePlayComposite { public const float SHEAR_WIDTH = 12f; private const float avatar_size = 36; private const float height = 60f; private static readonly Vector2 shear = new Vector2(SHEAR_WIDTH / height, 0); private FillFlowContainer avatarFlow; private CircularAvatar hostAvatar; private LinkFlowContainer hostText; private HiddenUserCount hiddenUsers; private OsuSpriteText totalCount; public DrawableRoomParticipantsList() { AutoSizeAxes = Axes.X; Height = height; } [BackgroundDependencyLoader] private void load(OverlayColourProvider colours) { InternalChildren = new Drawable[] { new Container { RelativeSizeAxes = Axes.Both, Masking = true, CornerRadius = 10, Shear = shear, Child = new Box { RelativeSizeAxes = Axes.Both, Colour = colours.Background4, } }, new FillFlowContainer { RelativeSizeAxes = Axes.Y, AutoSizeAxes = Axes.X, Children = new Drawable[] { new FillFlowContainer { RelativeSizeAxes = Axes.Y, AutoSizeAxes = Axes.X, Spacing = new Vector2(8), Padding = new MarginPadding { Left = 8, Right = 16 }, Children = new Drawable[] { hostAvatar = new CircularAvatar { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, }, hostText = new LinkFlowContainer { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, AutoSizeAxes = Axes.Both } } }, new Container { RelativeSizeAxes = Axes.Y, AutoSizeAxes = Axes.X, Children = new Drawable[] { new Container { RelativeSizeAxes = Axes.Both, Masking = true, CornerRadius = 10, Shear = shear, Child = new Box { RelativeSizeAxes = Axes.Both, Colour = colours.Background3, } }, new FillFlowContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(4), Padding = new MarginPadding { Left = 8, Right = 16 }, Children = new Drawable[] { new SpriteIcon { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Size = new Vector2(16), Icon = FontAwesome.Solid.User, }, totalCount = new OsuSpriteText { Font = OsuFont.Default.With(weight: FontWeight.Bold), Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, }, avatarFlow = new FillFlowContainer { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(4), Margin = new MarginPadding { Left = 4 }, }, hiddenUsers = new HiddenUserCount { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, } } } } }, } } }; } protected override void LoadComplete() { base.LoadComplete(); RecentParticipants.BindCollectionChanged(onParticipantsChanged, true); ParticipantCount.BindValueChanged(_ => { updateHiddenUsers(); totalCount.Text = ParticipantCount.Value.ToString(); }, true); Host.BindValueChanged(onHostChanged, true); } private int numberOfCircles = 4; /// /// The maximum number of circles visible (including the "hidden count" circle in the overflow case). /// public int NumberOfCircles { get => numberOfCircles; set { numberOfCircles = value; if (LoadState < LoadState.Loaded) return; // Reinitialising the list looks janky, but this is unlikely to be used in a setting where it's visible. clearUsers(); foreach (var u in RecentParticipants) addUser(u); updateHiddenUsers(); } } private void onParticipantsChanged(object sender, NotifyCollectionChangedEventArgs e) { switch (e.Action) { case NotifyCollectionChangedAction.Add: Debug.Assert(e.NewItems != null); foreach (var added in e.NewItems.OfType()) addUser(added); break; case NotifyCollectionChangedAction.Remove: Debug.Assert(e.OldItems != null); foreach (var removed in e.OldItems.OfType()) removeUser(removed); break; case NotifyCollectionChangedAction.Reset: clearUsers(); break; case NotifyCollectionChangedAction.Replace: case NotifyCollectionChangedAction.Move: // Easiest is to just reinitialise the whole list. These are unlikely to ever be use cases. clearUsers(); foreach (var u in RecentParticipants) addUser(u); break; } updateHiddenUsers(); } private int displayedCircles => avatarFlow.Count + (hiddenUsers.Count > 0 ? 1 : 0); private void addUser(APIUser user) { if (displayedCircles < NumberOfCircles) avatarFlow.Add(new CircularAvatar { User = user }); } private void removeUser(APIUser user) { avatarFlow.RemoveAll(a => a.User == user, true); } private void clearUsers() { avatarFlow.Clear(); updateHiddenUsers(); } private void updateHiddenUsers() { int hiddenCount = 0; if (RecentParticipants.Count > NumberOfCircles) hiddenCount = ParticipantCount.Value - NumberOfCircles + 1; hiddenUsers.Count = hiddenCount; if (displayedCircles > NumberOfCircles) avatarFlow.Remove(avatarFlow.Last(), true); else if (displayedCircles < NumberOfCircles) { var nextUser = RecentParticipants.FirstOrDefault(u => avatarFlow.All(a => a.User != u)); if (nextUser != null) addUser(nextUser); } } private void onHostChanged(ValueChangedEvent host) { hostAvatar.User = host.NewValue; hostText.Clear(); if (host.NewValue != null) { hostText.AddText("hosted by "); hostText.AddUserLink(host.NewValue); } } private partial class CircularAvatar : CompositeDrawable { public APIUser User { get => avatar.User; set => avatar.User = value; } private readonly UpdateableAvatar avatar = new UpdateableAvatar(showUsernameTooltip: true) { RelativeSizeAxes = Axes.Both }; [BackgroundDependencyLoader] private void load(OverlayColourProvider colours) { Size = new Vector2(avatar_size); InternalChild = new CircularContainer { RelativeSizeAxes = Axes.Both, Masking = true, Children = new Drawable[] { new Box { Colour = colours.Background5, RelativeSizeAxes = Axes.Both, }, avatar } }; } } public partial class HiddenUserCount : CompositeDrawable { public int Count { get => count; set { count = value; countText.Text = $"+{count}"; if (count > 0) Show(); else Hide(); } } private int count; private readonly SpriteText countText = new OsuSpriteText { Anchor = Anchor.Centre, Origin = Anchor.Centre, Font = OsuFont.Default.With(weight: FontWeight.Bold), }; [BackgroundDependencyLoader] private void load(OverlayColourProvider colours) { Size = new Vector2(avatar_size); Alpha = 0; InternalChild = new CircularContainer { RelativeSizeAxes = Axes.Both, Masking = true, Children = new Drawable[] { new Box { RelativeSizeAxes = Axes.Both, Colour = colours.Background5, }, countText } }; } } } }