diff --git a/osu.Game.Tests/Visual/Online/TestSceneChannelControlItem.cs b/osu.Game.Tests/Visual/Online/TestSceneChannelControlItem.cs new file mode 100644 index 0000000000..a1431f696b --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneChannelControlItem.cs @@ -0,0 +1,145 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +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.Game.Graphics.Sprites; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Chat; +using osu.Game.Overlays; +using osu.Game.Overlays.Chat.ChannelControl; +using osuTK; + +namespace osu.Game.Tests.Visual.Online +{ + [TestFixture] + public class TestSceneChannelControlItem : OsuTestScene + { + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink); + + [Cached] + private readonly Bindable selected = new Bindable(); + + private static List channels = new List + { + createPublicChannel("#public-channel"), + createPublicChannel("#public-channel-long-name"), + createPrivateChannel("test user", 2), + createPrivateChannel("test user long name", 3), + }; + + private readonly Dictionary channelMap = new Dictionary(); + + private FillFlowContainer flow; + private OsuSpriteText selectedText; + private OsuSpriteText leaveText; + + [SetUp] + public void SetUp() + { + Schedule(() => + { + Children = new Drawable[] + { + selectedText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Y = -140, + }, + leaveText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Y = -120, + }, + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(190, 200), + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background6, + }, + flow = new FillFlowContainer + { + Spacing = new Vector2(20), + RelativeSizeAxes = Axes.Both, + }, + }, + }, + }; + + selected.BindValueChanged(change => + { + selectedText.Text = $"Selected Channel: {change.NewValue?.Name ?? "[null]"}"; + }, true); + + foreach (var channel in channels) + { + var item = new ControlItem(channel); + flow.Add(item); + channelMap.Add(channel, item); + item.OnRequestSelect += channel => selected.Value = channel; + item.OnRequestLeave += leaveChannel; + } + }); + } + + [Test] + public void TestVisual() + { + AddStep("Unread Selected", () => + { + if (selected.Value != null) + channelMap[selected.Value].HasUnread = true; + }); + + AddStep("Read Selected", () => + { + if (selected.Value != null) + channelMap[selected.Value].HasUnread = false; + }); + + AddStep("Add Mention Selected", () => + { + if (selected.Value != null) + channelMap[selected.Value].MentionCount++; + }); + + AddStep("Add 98 Mentions Selected", () => + { + if (selected.Value != null) + channelMap[selected.Value].MentionCount += 98; + }); + + AddStep("Clear Mentions Selected", () => + { + if (selected.Value != null) + channelMap[selected.Value].MentionCount = 0; + }); + } + + private void leaveChannel(Channel channel) + { + leaveText.Text = $"OnRequestLeave: {channel.Name}"; + leaveText.FadeIn().Then().FadeOut(1000, Easing.InQuint); + } + + private static Channel createPublicChannel(string name) => + new Channel { Name = name, Type = ChannelType.Public, Id = 1234 }; + + private static Channel createPrivateChannel(string username, int id) + => new Channel(new APIUser { Id = id, Username = username }); + } +} diff --git a/osu.Game/Overlays/Chat/ChannelControl/ControlItem.cs b/osu.Game/Overlays/Chat/ChannelControl/ControlItem.cs new file mode 100644 index 0000000000..559f75f198 --- /dev/null +++ b/osu.Game/Overlays/Chat/ChannelControl/ControlItem.cs @@ -0,0 +1,167 @@ +// 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.Shapes; +using osu.Framework.Input.Events; +using osu.Game.Graphics.Containers; +using osu.Game.Online.Chat; + +namespace osu.Game.Overlays.Chat.ChannelControl +{ + public class ControlItem : OsuClickableContainer + { + public event Action? OnRequestSelect; + public event Action? OnRequestLeave; + + public int MentionCount + { + get => mention?.MentionCount ?? 0; + set + { + if (mention != null) + mention.MentionCount = value; + } + } + + public bool HasUnread + { + get => text?.HasUnread ?? false; + set + { + if (text != null) + text.HasUnread = value; + } + } + + private Box? hoverBox; + private Box? selectBox; + private ControlItemText? text; + private ControlItemMention? mention; + private ControlItemClose? close; + + [Resolved] + private Bindable? selectedChannel { get; set; } + + [Resolved] + private OverlayColourProvider? colourProvider { get; set; } + + private readonly Channel channel; + + public ControlItem(Channel channel) + { + this.channel = channel; + } + + [BackgroundDependencyLoader] + private void load() + { + Height = 30; + RelativeSizeAxes = Axes.X; + + Children = new Drawable[] + { + hoverBox = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider!.Background3, + Alpha = 0f, + }, + selectBox = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider!.Background4, + Alpha = 0f, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = 18, Right = 5 }, + Child = new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new Drawable[] + { + createAvatar(), + text = new ControlItemText(channel) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + mention = new ControlItemMention + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding { Right = 3 }, + }, + close = new ControlItemClose + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding { Right = 3 }, + } + } + }, + }, + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + selectedChannel?.BindValueChanged(change => + { + if (change.NewValue == channel) + selectBox?.Show(); + else + selectBox?.Hide(); + }, true); + + Action = () => OnRequestSelect?.Invoke(channel); + close!.Action = () => OnRequestLeave?.Invoke(channel); + } + + protected override bool OnHover(HoverEvent e) + { + hoverBox?.Show(); + close?.Show(); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + hoverBox?.Hide(); + close?.Hide(); + base.OnHoverLost(e); + } + + private Drawable createAvatar() + { + if (channel.Type != ChannelType.PM) + return Drawable.Empty(); + + return new ControlItemAvatar(channel) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }; + } + } +} diff --git a/osu.Game/Overlays/Chat/ChannelControl/ControlItemAvatar.cs b/osu.Game/Overlays/Chat/ChannelControl/ControlItemAvatar.cs new file mode 100644 index 0000000000..9192f20cb1 --- /dev/null +++ b/osu.Game/Overlays/Chat/ChannelControl/ControlItemAvatar.cs @@ -0,0 +1,60 @@ +// 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.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Online.Chat; +using osu.Game.Users.Drawables; +using osuTK; + +namespace osu.Game.Overlays.Chat.ChannelControl +{ + public class ControlItemAvatar : CircularContainer + { + private DrawableAvatar? avatar; + private readonly Channel channel; + + public ControlItemAvatar(Channel channel) + { + this.channel = channel; + } + + [BackgroundDependencyLoader] + private void load() + { + Size = new Vector2(20); + Margin = new MarginPadding { Right = 5 }; + Masking = true; + + Children = new Drawable[] + { + new SpriteIcon + { + Icon = FontAwesome.Solid.At, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = Colour4.Black, + RelativeSizeAxes = Axes.Both, + Alpha = 0.2f, + }, + new DelayedLoadWrapper(avatar = new DrawableAvatar(channel.Users.First()) + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }), + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + avatar!.OnLoadComplete += d => d.FadeInFromZero(300, Easing.OutQuint); + } + } +} diff --git a/osu.Game/Overlays/Chat/ChannelControl/ControlItemClose.cs b/osu.Game/Overlays/Chat/ChannelControl/ControlItemClose.cs new file mode 100644 index 0000000000..4b190e18fd --- /dev/null +++ b/osu.Game/Overlays/Chat/ChannelControl/ControlItemClose.cs @@ -0,0 +1,55 @@ +// 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.Sprites; +using osu.Framework.Input.Events; +using osu.Game.Graphics.Containers; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Overlays.Chat.ChannelControl +{ + public class ControlItemClose : OsuClickableContainer + { + private readonly SpriteIcon icon; + + public ControlItemClose() + { + Alpha = 0f; + Size = new Vector2(20); + Child = icon = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(0.75f), + Icon = FontAwesome.Solid.TimesCircle, + RelativeSizeAxes = Axes.Both, + }; + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + icon.ScaleTo(0.5f, 1000, Easing.OutQuint); + return base.OnMouseDown(e); + } + + protected override void OnMouseUp(MouseUpEvent e) + { + icon.ScaleTo(0.75f, 1000, Easing.OutElastic); + base.OnMouseUp(e); + } + + protected override bool OnHover(HoverEvent e) + { + icon.FadeColour(Color4.Red, 200, Easing.OutQuint); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + icon.FadeColour(Color4.White, 200, Easing.OutQuint); + base.OnHoverLost(e); + } + } +} diff --git a/osu.Game/Overlays/Chat/ChannelControl/ControlItemMention.cs b/osu.Game/Overlays/Chat/ChannelControl/ControlItemMention.cs new file mode 100644 index 0000000000..12de154faa --- /dev/null +++ b/osu.Game/Overlays/Chat/ChannelControl/ControlItemMention.cs @@ -0,0 +1,77 @@ +// 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 osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osuTK; + +namespace osu.Game.Overlays.Chat.ChannelControl +{ + public class ControlItemMention : CircularContainer + { + private int mentionCount = 0; + public int MentionCount + { + get => mentionCount; + set + { + if (value == mentionCount) + return; + + mentionCount = value; + updateText(); + } + } + + private OsuSpriteText? countText; + + [Resolved] + private OverlayColourProvider? colourProvider { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + Masking = true; + Size = new Vector2(20, 12); + Alpha = 0f; + + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider!.Colour1, + }, + countText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Torus.With(size: 11, weight: FontWeight.Bold), + Margin = new MarginPadding { Bottom = 1 }, + Colour = colourProvider.Background5, + }, + }; + + updateText(); + } + + private void updateText() + { + if (mentionCount > 99) + countText!.Text = "99+"; + else + countText!.Text = mentionCount.ToString(); + + if (mentionCount > 0) + this.Show(); + else + this.Hide(); + } + } +} diff --git a/osu.Game/Overlays/Chat/ChannelControl/ControlItemText.cs b/osu.Game/Overlays/Chat/ChannelControl/ControlItemText.cs new file mode 100644 index 0000000000..2b8f50e184 --- /dev/null +++ b/osu.Game/Overlays/Chat/ChannelControl/ControlItemText.cs @@ -0,0 +1,71 @@ +// 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 osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Chat; + +namespace osu.Game.Overlays.Chat.ChannelControl +{ + public class ControlItemText : Container + { + public bool HasUnread + { + get => hasUnread; + set + { + if (hasUnread == value) + return; + + hasUnread = value; + updateText(); + } + } + + private bool hasUnread = false; + private OsuSpriteText? text; + + [Resolved] + private OverlayColourProvider? colourProvider { get; set; } + + private readonly Channel channel; + + public ControlItemText(Channel channel) + { + this.channel = channel; + } + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.Both; + Child = text = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = channel.Type == ChannelType.Public ? $"# {channel.Name.Substring(1)}" : channel.Name, + Font = OsuFont.Torus.With(size: 17, weight: FontWeight.SemiBold), + Colour = colourProvider!.Light3, + Margin = new MarginPadding { Bottom = 2 }, + RelativeSizeAxes = Axes.X, + Truncate = true, + }; + } + + private void updateText() + { + if (!IsLoaded) + return; + + if (HasUnread) + text!.Colour = Colour4.White; + else + text!.Colour = colourProvider!.Light3; + } + } +}