diff --git a/osu.Game/Overlays/ExpandingButtonContainer.cs b/osu.Game/Overlays/ExpandingButtonContainer.cs index 4eb8c47a1f..d7ff285707 100644 --- a/osu.Game/Overlays/ExpandingButtonContainer.cs +++ b/osu.Game/Overlays/ExpandingButtonContainer.cs @@ -1,141 +1,23 @@ // 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 System.Linq; -using osu.Framework; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Input.Events; -using osu.Framework.Testing; -using osu.Framework.Threading; -using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; -using osuTK; namespace osu.Game.Overlays { - public abstract class ExpandingButtonContainer : Container, IStateful + /// + /// An with a long hover expansion delay for buttons. + /// + /// + /// Mostly used for buttons with explanatory labels, in which the label would display after a "long hover". + /// + public class ExpandingButtonContainer : ExpandingControlContainer { - private readonly float contractedWidth; - private readonly float expandedWidth; - - public event Action StateChanged; - - protected override Container Content => FillFlow; - - protected FillFlowContainer FillFlow { get; } - protected ExpandingButtonContainer(float contractedWidth, float expandedWidth) + : base(contractedWidth, expandedWidth) { - this.contractedWidth = contractedWidth; - this.expandedWidth = expandedWidth; - - RelativeSizeAxes = Axes.Y; - Width = contractedWidth; - - InternalChildren = new Drawable[] - { - new SidebarScrollContainer - { - Children = new[] - { - FillFlow = new FillFlowContainer - { - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Direction = FillDirection.Vertical, - } - } - }, - }; } - private ScheduledDelegate expandEvent; - private ExpandedState state; - - protected override bool OnHover(HoverEvent e) - { - queueExpandIfHovering(); - return true; - } - - protected override void OnHoverLost(HoverLostEvent e) - { - expandEvent?.Cancel(); - hoveredButton = null; - State = ExpandedState.Contracted; - - base.OnHoverLost(e); - } - - protected override bool OnMouseMove(MouseMoveEvent e) - { - queueExpandIfHovering(); - return base.OnMouseMove(e); - } - - private class SidebarScrollContainer : OsuScrollContainer - { - public SidebarScrollContainer() - { - RelativeSizeAxes = Axes.Both; - ScrollbarVisible = false; - } - } - - public ExpandedState State - { - get => state; - set - { - expandEvent?.Cancel(); - - if (state == value) return; - - state = value; - - switch (state) - { - default: - this.ResizeTo(new Vector2(contractedWidth, Height), 500, Easing.OutQuint); - break; - - case ExpandedState.Expanded: - this.ResizeTo(new Vector2(expandedWidth, Height), 500, Easing.OutQuint); - break; - } - - StateChanged?.Invoke(State); - } - } - - private Drawable hoveredButton; - - private void queueExpandIfHovering() - { - // if the same button is hovered, let the scheduled expand play out.. - if (hoveredButton?.IsHovered == true) - return; - - // ..otherwise check whether a new button is hovered, and if so, queue a new hover operation. - - // usually we wouldn't use ChildrenOfType in implementations, but this is the simplest way - // to handle cases like the editor where the buttons may be nested within a child hierarchy. - hoveredButton = FillFlow.ChildrenOfType().FirstOrDefault(c => c.IsHovered); - - expandEvent?.Cancel(); - - if (hoveredButton?.IsHovered == true && State != ExpandedState.Expanded) - expandEvent = Scheduler.AddDelayed(() => State = ExpandedState.Expanded, 750); - } - } - - public enum ExpandedState - { - Contracted, - Expanded, + protected override double HoverExpansionDelay => 750; } } diff --git a/osu.Game/Overlays/ExpandingControlContainer.cs b/osu.Game/Overlays/ExpandingControlContainer.cs new file mode 100644 index 0000000000..8e02cab923 --- /dev/null +++ b/osu.Game/Overlays/ExpandingControlContainer.cs @@ -0,0 +1,124 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; +using osu.Framework.Testing; +using osu.Framework.Threading; +using osu.Game.Graphics.Containers; +using osu.Game.Overlays.Settings; + +namespace osu.Game.Overlays +{ + /// + /// Represents a with the ability to expand/contract when hovering the controls within it. + /// + /// The type of UI control to lookup for hover expansion. + public class ExpandingControlContainer : Container, IExpandingContainer + where TControl : class, IDrawable + { + private readonly float contractedWidth; + private readonly float expandedWidth; + + public BindableBool Expanded { get; } = new BindableBool(); + + /// + /// Delay before the container switches to expanded state from hover. + /// + protected virtual double HoverExpansionDelay => 0; + + protected override Container Content => FillFlow; + + protected FillFlowContainer FillFlow { get; } + + protected ExpandingControlContainer(float contractedWidth, float expandedWidth) + { + this.contractedWidth = contractedWidth; + this.expandedWidth = expandedWidth; + + RelativeSizeAxes = Axes.Y; + Width = contractedWidth; + + InternalChild = new OsuScrollContainer + { + RelativeSizeAxes = Axes.Both, + ScrollbarVisible = false, + Child = FillFlow = new FillFlowContainer + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + }, + }; + } + + private ScheduledDelegate hoverExpandEvent; + private TControl activeControl; + + protected override void LoadComplete() + { + base.LoadComplete(); + + Expanded.BindValueChanged(v => + { + this.ResizeWidthTo(v.NewValue ? expandedWidth : contractedWidth, 500, Easing.OutQuint); + }, true); + } + + protected override void Update() + { + base.Update(); + + // if the container was expanded from hovering over a control, we have to check per-frame whether we can contract it back. + // that's because contracting the container depends not only on whether it's no longer hovered, + // but also on whether the hovered control is no longer in a dragged state (if it was). + if (hoverExpandEvent != null && !IsHovered && (activeControl == null || !isControlActive(activeControl))) + { + hoverExpandEvent?.Cancel(); + + Expanded.Value = false; + hoverExpandEvent = null; + activeControl = null; + } + } + + protected override bool OnHover(HoverEvent e) + { + queueExpandIfHovering(); + return true; + } + + protected override bool OnMouseMove(MouseMoveEvent e) + { + queueExpandIfHovering(); + return base.OnMouseMove(e); + } + + private void queueExpandIfHovering() + { + // if the same control is hovered or dragged, let the scheduled expand play out.. + if (activeControl != null && isControlActive(activeControl)) + return; + + // ..otherwise check whether a new control is hovered, and if so, queue a new hover operation. + hoverExpandEvent?.Cancel(); + + // usually we wouldn't use ChildrenOfType in implementations, but this is the simplest way + // to handle cases like the editor where the controls may be nested within a child hierarchy. + activeControl = FillFlow.ChildrenOfType().FirstOrDefault(isControlActive); + + if (activeControl != null && !Expanded.Value) + hoverExpandEvent = Scheduler.AddDelayed(() => Expanded.Value = true, HoverExpansionDelay); + } + + /// + /// Whether the given control is currently active, by checking whether it's hovered or dragged. + /// + private bool isControlActive(TControl control) => control.IsHovered || control.IsDragged; + } +} diff --git a/osu.Game/Overlays/IExpandingContainer.cs b/osu.Game/Overlays/IExpandingContainer.cs new file mode 100644 index 0000000000..ec5f0c90f4 --- /dev/null +++ b/osu.Game/Overlays/IExpandingContainer.cs @@ -0,0 +1,16 @@ +// 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.Graphics.Containers; + +namespace osu.Game.Overlays +{ + /// + /// A target expanding container that should be resolved by children s to propagate state changes. + /// + [Cached(typeof(IExpandingContainer))] + public interface IExpandingContainer : IContainer, IExpandable + { + } +} diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs index ba7118cffe..b11b6fde27 100644 --- a/osu.Game/Overlays/SettingsPanel.cs +++ b/osu.Game/Overlays/SettingsPanel.cs @@ -265,7 +265,7 @@ namespace osu.Game.Overlays return; SectionsContainer.ScrollTo(section); - Sidebar.State = ExpandedState.Contracted; + Sidebar.Expanded.Value = false; }, }; }