diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs new file mode 100644 index 0000000000..064d6f82fd --- /dev/null +++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs @@ -0,0 +1,80 @@ +// 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 osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Timing; +using osu.Game.Overlays.Toolbar; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual.Menus +{ + [TestFixture] + public class TestSceneToolbarClock : OsuManualInputManagerTestScene + { + private readonly Container mainContainer; + + public TestSceneToolbarClock() + { + Children = new Drawable[] + { + mainContainer = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + Height = Toolbar.HEIGHT, + Children = new Drawable[] + { + new Box + { + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, + }, + new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + new Box + { + Colour = Color4.DarkRed, + RelativeSizeAxes = Axes.Y, + Width = 2, + }, + new ToolbarClock(), + new Box + { + Colour = Color4.DarkRed, + RelativeSizeAxes = Axes.Y, + Width = 2, + }, + } + }, + } + }, + }; + + AddSliderStep("scale", 0.5, 4, 1, scale => mainContainer.Scale = new Vector2((float)scale)); + } + + [Test] + public void TestRealGameTime() + { + AddStep("Set game time real", () => mainContainer.Clock = Clock); + } + + [Test] + public void TestLongGameTime() + { + AddStep("Set game time long", () => mainContainer.Clock = new FramedOffsetClock(Clock, false) { Offset = 3600.0 * 24 * 1000 * 98 }); + } + } +} diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index c279ce1220..e8f13ba902 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -44,6 +44,8 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.ChatDisplayHeight, ChatOverlay.DEFAULT_HEIGHT, 0.2f, 1f); + SetDefault(OsuSetting.ToolbarClockDisplayMode, ToolbarClockDisplayMode.Full); + // Online settings SetDefault(OsuSetting.Username, string.Empty); SetDefault(OsuSetting.Token, string.Empty); @@ -295,6 +297,7 @@ namespace osu.Game.Configuration RandomSelectAlgorithm, ShowFpsDisplay, ChatDisplayHeight, + ToolbarClockDisplayMode, Version, ShowConvertedBeatmaps, Skin, diff --git a/osu.Game/Configuration/ToolbarClockDisplayMode.cs b/osu.Game/Configuration/ToolbarClockDisplayMode.cs new file mode 100644 index 0000000000..2f42f7a9b5 --- /dev/null +++ b/osu.Game/Configuration/ToolbarClockDisplayMode.cs @@ -0,0 +1,13 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Configuration +{ + public enum ToolbarClockDisplayMode + { + Analog, + Digital, + DigitalWithRuntime, + Full + } +} diff --git a/osu.Game/Overlays/Toolbar/AnalogClockDisplay.cs b/osu.Game/Overlays/Toolbar/AnalogClockDisplay.cs new file mode 100644 index 0000000000..9228900e99 --- /dev/null +++ b/osu.Game/Overlays/Toolbar/AnalogClockDisplay.cs @@ -0,0 +1,159 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Overlays.Toolbar +{ + public class AnalogClockDisplay : ClockDisplay + { + private const float hand_thickness = 2.4f; + + private Drawable hour; + private Drawable minute; + private Drawable second; + + [BackgroundDependencyLoader] + private void load() + { + Size = new Vector2(22); + + InternalChildren = new[] + { + new CircularContainer + { + RelativeSizeAxes = Axes.Both, + Masking = true, + BorderThickness = 2, + BorderColour = Color4.White, + Child = new Box + { + AlwaysPresent = true, + Alpha = 0, + RelativeSizeAxes = Axes.Both + }, + }, + hour = new LargeHand(0.34f), + minute = new LargeHand(0.48f), + second = new SecondHand(), + new CentreCircle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }; + } + + protected override void UpdateDisplay(DateTimeOffset now) + { + float secondFractional = now.Second / 60f; + float minuteFractional = (now.Minute + secondFractional) / 60f; + float hourFractional = ((minuteFractional + now.Hour) % 12) / 12f; + + updateRotation(hour, hourFractional); + updateRotation(minute, minuteFractional); + updateRotation(second, secondFractional); + } + + private void updateRotation(Drawable hand, float fraction) + { + const float duration = 320; + + float rotation = fraction * 360 - 90; + + if (Math.Abs(hand.Rotation - rotation) > 180) + hand.RotateTo(rotation); + else + hand.RotateTo(rotation, duration, Easing.OutElastic); + } + + private class CentreCircle : CompositeDrawable + { + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + InternalChildren = new Drawable[] + { + new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(hand_thickness), + Colour = Color4.White, + }, + new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(hand_thickness * 0.7f), + Colour = colours.PinkLight, + }, + }; + } + } + + private class SecondHand : CompositeDrawable + { + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + RelativeSizeAxes = Axes.X; + Width = 0.66f; + + Height = hand_thickness * 0.7f; + Anchor = Anchor.Centre; + Origin = Anchor.Custom; + + OriginPosition = new Vector2(Height * 2, Height / 2); + + InternalChildren = new Drawable[] + { + new Circle + { + Colour = colours.PinkLight, + RelativeSizeAxes = Axes.Both, + }, + }; + } + } + + private class LargeHand : CompositeDrawable + { + public LargeHand(float length) + { + Width = length; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Anchor = Anchor.Centre; + Origin = Anchor.Custom; + + OriginPosition = new Vector2(hand_thickness / 2); // offset x also, to ensure the centre of the line is centered on the face. + + Height = hand_thickness; + + InternalChildren = new Drawable[] + { + new Circle + { + Colour = Color4.White, + RelativeSizeAxes = Axes.Both, + BorderThickness = 0.7f, + BorderColour = colours.Gray2, + }, + }; + + RelativeSizeAxes = Axes.X; + } + } + } +} diff --git a/osu.Game/Overlays/Toolbar/ClockDisplay.cs b/osu.Game/Overlays/Toolbar/ClockDisplay.cs new file mode 100644 index 0000000000..c1befbb198 --- /dev/null +++ b/osu.Game/Overlays/Toolbar/ClockDisplay.cs @@ -0,0 +1,28 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Overlays.Toolbar +{ + public abstract class ClockDisplay : CompositeDrawable + { + private int? lastSecond; + + protected override void Update() + { + base.Update(); + + var now = DateTimeOffset.Now; + + if (now.Second != lastSecond) + { + lastSecond = now.Second; + UpdateDisplay(now); + } + } + + protected abstract void UpdateDisplay(DateTimeOffset now); + } +} diff --git a/osu.Game/Overlays/Toolbar/DigitalClockDisplay.cs b/osu.Game/Overlays/Toolbar/DigitalClockDisplay.cs new file mode 100644 index 0000000000..090f8c4a0f --- /dev/null +++ b/osu.Game/Overlays/Toolbar/DigitalClockDisplay.cs @@ -0,0 +1,64 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osuTK; + +namespace osu.Game.Overlays.Toolbar +{ + public class DigitalClockDisplay : ClockDisplay + { + private OsuSpriteText realTime; + private OsuSpriteText gameTime; + + private bool showRuntime = true; + + public bool ShowRuntime + { + get => showRuntime; + set + { + if (showRuntime == value) + return; + + showRuntime = value; + updateMetrics(); + } + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + AutoSizeAxes = Axes.Y; + + InternalChildren = new Drawable[] + { + realTime = new OsuSpriteText(), + gameTime = new OsuSpriteText + { + Y = 14, + Colour = colours.PinkLight, + Scale = new Vector2(0.6f) + } + }; + + updateMetrics(); + } + + protected override void UpdateDisplay(DateTimeOffset now) + { + realTime.Text = $"{now:HH:mm:ss}"; + gameTime.Text = $"running {new TimeSpan(TimeSpan.TicksPerSecond * (int)(Clock.CurrentTime / 1000)):c}"; + } + + private void updateMetrics() + { + Width = showRuntime ? 66 : 45; // Allows for space for game time up to 99 days (in the padding area since this is quite rare). + gameTime.FadeTo(showRuntime ? 1 : 0); + } + } +} diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs index 776f7ad7b7..b7fb2e45be 100644 --- a/osu.Game/Overlays/Toolbar/Toolbar.cs +++ b/osu.Game/Overlays/Toolbar/Toolbar.cs @@ -104,6 +104,7 @@ namespace osu.Game.Overlays.Toolbar // Icon = FontAwesome.Solid.search //}, userButton = new ToolbarUserButton(), + new ToolbarClock(), new ToolbarNotificationButton(), } } diff --git a/osu.Game/Overlays/Toolbar/ToolbarClock.cs b/osu.Game/Overlays/Toolbar/ToolbarClock.cs new file mode 100644 index 0000000000..ad5c9ac7a1 --- /dev/null +++ b/osu.Game/Overlays/Toolbar/ToolbarClock.cs @@ -0,0 +1,101 @@ +// 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.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; +using osu.Game.Configuration; +using osuTK; + +namespace osu.Game.Overlays.Toolbar +{ + public class ToolbarClock : CompositeDrawable + { + private Bindable clockDisplayMode; + + private DigitalClockDisplay digital; + private AnalogClockDisplay analog; + + public ToolbarClock() + { + RelativeSizeAxes = Axes.Y; + AutoSizeAxes = Axes.X; + + Padding = new MarginPadding(10); + } + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + clockDisplayMode = config.GetBindable(OsuSetting.ToolbarClockDisplayMode); + + InternalChild = new FillFlowContainer + { + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + Children = new Drawable[] + { + analog = new AnalogClockDisplay + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + digital = new DigitalClockDisplay + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + clockDisplayMode.BindValueChanged(displayMode => + { + bool showAnalog = displayMode.NewValue == ToolbarClockDisplayMode.Analog || displayMode.NewValue == ToolbarClockDisplayMode.Full; + bool showDigital = displayMode.NewValue != ToolbarClockDisplayMode.Analog; + bool showRuntime = displayMode.NewValue == ToolbarClockDisplayMode.DigitalWithRuntime || displayMode.NewValue == ToolbarClockDisplayMode.Full; + + digital.FadeTo(showDigital ? 1 : 0); + digital.ShowRuntime = showRuntime; + + analog.FadeTo(showAnalog ? 1 : 0); + }, true); + } + + protected override bool OnClick(ClickEvent e) + { + cycleDisplayMode(); + return true; + } + + private void cycleDisplayMode() + { + switch (clockDisplayMode.Value) + { + case ToolbarClockDisplayMode.Analog: + clockDisplayMode.Value = ToolbarClockDisplayMode.Full; + break; + + case ToolbarClockDisplayMode.Digital: + clockDisplayMode.Value = ToolbarClockDisplayMode.Analog; + break; + + case ToolbarClockDisplayMode.DigitalWithRuntime: + clockDisplayMode.Value = ToolbarClockDisplayMode.Digital; + break; + + case ToolbarClockDisplayMode.Full: + clockDisplayMode.Value = ToolbarClockDisplayMode.DigitalWithRuntime; + break; + } + } + } +}