diff --git a/osu.Game.Tests/Visual/Online/TestSceneChannelListing.cs b/osu.Game.Tests/Visual/Online/TestSceneChannelListing.cs new file mode 100644 index 0000000000..e521db1c9d --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneChannelListing.cs @@ -0,0 +1,90 @@ +// Copyright (c) ppy Pty Ltd . 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 NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Chat; +using osu.Game.Overlays; +using osu.Game.Overlays.Chat.Listing; +using osuTK; + +namespace osu.Game.Tests.Visual.Online +{ + [TestFixture] + public class TestSceneChannelListing : OsuTestScene + { + [Cached] + private readonly OverlayColourProvider overlayColours = new OverlayColourProvider(OverlayColourScheme.Pink); + + private SearchTextBox search; + private ChannelListing listing; + + [SetUp] + public void SetUp() + { + Schedule(() => + { + Children = new Drawable[] + { + search = new SearchTextBox + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Width = 300, + Margin = new MarginPadding { Top = 100 }, + }, + listing = new ChannelListing + { + Size = new Vector2(800, 400), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + }; + listing.Show(); + search.Current.ValueChanged += term => listing.SearchTerm = term.NewValue; + }); + } + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("Add Join/Leave callbacks", () => + { + listing.OnRequestJoin += channel => channel.Joined.Value = true; + listing.OnRequestLeave += channel => channel.Joined.Value = false; + }); + } + + [Test] + public void TestAddRandomChannels() + { + AddStep("Add Random Channels", () => + { + listing.UpdateAvailableChannels(createRandomChannels(20)); + }); + } + + private Channel createRandomChannel() + { + int id = RNG.Next(0, 10000); + return new Channel + { + Name = $"#channel-{id}", + Topic = RNG.Next(4) < 3 ? $"We talk about the number {id} here" : null, + Type = ChannelType.Public, + Id = id, + }; + } + + private List createRandomChannels(int num) + => Enumerable.Range(0, num) + .Select(_ => createRandomChannel()) + .ToList(); + } +} diff --git a/osu.Game/Overlays/Chat/Listing/ChannelListing.cs b/osu.Game/Overlays/Chat/Listing/ChannelListing.cs new file mode 100644 index 0000000000..732c78de15 --- /dev/null +++ b/osu.Game/Overlays/Chat/Listing/ChannelListing.cs @@ -0,0 +1,79 @@ +// 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 System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics.Containers; +using osu.Game.Online.Chat; + +namespace osu.Game.Overlays.Chat.Listing +{ + public class ChannelListing : VisibilityContainer + { + public event Action? OnRequestJoin; + public event Action? OnRequestLeave; + + public string SearchTerm + { + get => flow.SearchTerm; + set => flow.SearchTerm = value; + } + + private SearchContainer flow = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4, + }, + new OsuScrollContainer + { + RelativeSizeAxes = Axes.Both, + ScrollbarAnchor = Anchor.TopRight, + Child = flow = new SearchContainer + { + Direction = FillDirection.Vertical, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding + { + Vertical = 13, + Horizontal = 15, + }, + }, + }, + }; + } + + public void UpdateAvailableChannels(IEnumerable newChannels) + { + flow.ChildrenEnumerable = newChannels.Where(c => c.Type == ChannelType.Public) + .Select(c => new ChannelListingItem(c)); + + foreach (var item in flow.Children) + { + item.OnRequestJoin += channel => OnRequestJoin?.Invoke(channel); + item.OnRequestLeave += channel => OnRequestLeave?.Invoke(channel); + } + } + + protected override void PopIn() => this.FadeIn(); + + protected override void PopOut() => this.FadeOut(); + } +} diff --git a/osu.Game/Overlays/Chat/Listing/ChannelListingItem.cs b/osu.Game/Overlays/Chat/Listing/ChannelListingItem.cs new file mode 100644 index 0000000000..526cbcda87 --- /dev/null +++ b/osu.Game/Overlays/Chat/Listing/ChannelListingItem.cs @@ -0,0 +1,173 @@ +// 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 System.Collections.Generic; +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.Framework.Input.Events; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Chat; +using osuTK; + +namespace osu.Game.Overlays.Chat.Listing +{ + public class ChannelListingItem : OsuClickableContainer, IFilterable + { + public event Action? OnRequestJoin; + public event Action? OnRequestLeave; + + public bool FilteringActive { get; set; } + public IEnumerable FilterTerms => new[] { channel.Name, channel.Topic ?? string.Empty }; + public bool MatchingFilter { set => this.FadeTo(value ? 1f : 0f, 100); } + + private readonly Channel channel; + + private Box hoverBox = null!; + private SpriteIcon checkbox = null!; + private OsuSpriteText channelText = null!; + private OsuSpriteText topicText = null!; + private IBindable channelJoined = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + private const float text_size = 18; + private const float icon_size = 14; + + private const float vertical_margin = 1.5f; + + public ChannelListingItem(Channel channel) + { + this.channel = channel; + } + + [BackgroundDependencyLoader] + private void load() + { + Masking = true; + CornerRadius = 5; + RelativeSizeAxes = Axes.X; + Height = 20 + (vertical_margin * 2); + + Children = new Drawable[] + { + hoverBox = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background3, + Margin = new MarginPadding { Vertical = vertical_margin }, + Alpha = 0f, + }, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 40), + new Dimension(GridSizeMode.Absolute, 200), + new Dimension(GridSizeMode.Absolute, 400), + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + }, + Content = new[] + { + new Drawable[] + { + checkbox = new SpriteIcon + { + Alpha = 0, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.Check, + Size = new Vector2(icon_size), + }, + channelText = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = channel.Name, + Font = OsuFont.Torus.With(size: text_size, weight: FontWeight.SemiBold), + Margin = new MarginPadding { Bottom = 2 }, + }, + topicText = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = channel.Topic, + Font = OsuFont.Torus.With(size: text_size), + Margin = new MarginPadding { Bottom = 2 }, + }, + new SpriteIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Icon = FontAwesome.Solid.User, + Size = new Vector2(icon_size), + Margin = new MarginPadding { Right = 5 }, + Colour = colourProvider.Light3, + }, + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = "0", + Font = OsuFont.Torus.With(size: text_size), + Margin = new MarginPadding { Bottom = 2 }, + Colour = colourProvider.Light3, + }, + }, + }, + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + channelJoined = channel.Joined.GetBoundCopy(); + channelJoined.BindValueChanged(change => + { + const double duration = 500; + + if (change.NewValue) + { + checkbox.FadeIn(duration, Easing.OutQuint); + checkbox.ScaleTo(1f, duration, Easing.OutElastic); + channelText.Colour = Colour4.White; + topicText.Colour = Colour4.White; + } + else + { + checkbox.FadeOut(duration, Easing.OutQuint); + checkbox.ScaleTo(0.8f, duration, Easing.OutQuint); + channelText.Colour = colourProvider.Light3; + topicText.Colour = colourProvider.Content2; + } + }, true); + + Action = () => (channelJoined.Value ? OnRequestLeave : OnRequestJoin)?.Invoke(channel); + } + + protected override bool OnHover(HoverEvent e) + { + hoverBox.Show(); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + hoverBox.FadeOut(300, Easing.OutQuint); + base.OnHoverLost(e); + } + } +}