diff --git a/osu.Game/Extensions/DrawableExtensions.cs b/osu.Game/Extensions/DrawableExtensions.cs index 2ac6e6ff22..52d8230fb6 100644 --- a/osu.Game/Extensions/DrawableExtensions.cs +++ b/osu.Game/Extensions/DrawableExtensions.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; using osu.Framework.Threading; using osu.Game.Screens.Play.HUD; +using osu.Game.Skinning; using osuTK; namespace osu.Game.Extensions @@ -57,6 +58,9 @@ namespace osu.Game.Extensions component.Anchor = info.Anchor; component.Origin = info.Origin; + if (component is ISkinnableDrawable skinnable) + skinnable.UsesFixedAnchor = info.UsesFixedAnchor; + if (component is Container container) { foreach (var child in info.Children) diff --git a/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs b/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs index 45ba05e036..324e5d43b5 100644 --- a/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs @@ -12,6 +12,8 @@ namespace osu.Game.Screens.Play.HUD [Resolved(canBeNull: true)] private HUDOverlay hud { get; set; } + public bool UsesFixedAnchor { get; set; } + [BackgroundDependencyLoader] private void load(OsuColour colours) { diff --git a/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs b/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs index c4575c5ad0..718ae24cf1 100644 --- a/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs @@ -17,6 +17,8 @@ namespace osu.Game.Screens.Play.HUD [Resolved(canBeNull: true)] private HUDOverlay hud { get; set; } + public bool UsesFixedAnchor { get; set; } + public DefaultComboCounter() { Current.Value = DisplayedCount = 0; diff --git a/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs b/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs index ed297f0ffc..4f93868a66 100644 --- a/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs @@ -72,6 +72,8 @@ namespace osu.Game.Screens.Play.HUD } } + public bool UsesFixedAnchor { get; set; } + public DefaultHealthDisplay() { Size = new Vector2(1, 5); diff --git a/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs b/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs index 16e3642181..63de5c8de5 100644 --- a/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs @@ -20,6 +20,8 @@ namespace osu.Game.Screens.Play.HUD [Resolved(canBeNull: true)] private HUDOverlay hud { get; set; } + public bool UsesFixedAnchor { get; set; } + [BackgroundDependencyLoader] private void load(OsuColour colours) { diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs index 9844b9f10d..788ba5be8c 100644 --- a/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs +++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs @@ -22,6 +22,8 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters [Resolved] private OsuColour colours { get; set; } + public bool UsesFixedAnchor { get; set; } + [BackgroundDependencyLoader(true)] private void load(DrawableRuleset drawableRuleset) { diff --git a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs index 1737634e31..acff949353 100644 --- a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs @@ -59,6 +59,8 @@ namespace osu.Game.Screens.Play.HUD set => counterContainer.Alpha = value ? 1 : 0; } + public bool UsesFixedAnchor { get; set; } + public LegacyComboCounter() { AutoSizeAxes = Axes.Both; diff --git a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs index e08044b14c..b64e5ca98f 100644 --- a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs +++ b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs @@ -32,6 +32,9 @@ namespace osu.Game.Screens.Play.HUD public Anchor Origin { get; set; } + /// + public bool UsesFixedAnchor { get; set; } + public List Children { get; } = new List(); [JsonConstructor] @@ -53,6 +56,9 @@ namespace osu.Game.Screens.Play.HUD Anchor = component.Anchor; Origin = component.Origin; + if (component is ISkinnableDrawable skinnable) + UsesFixedAnchor = skinnable.UsesFixedAnchor; + if (component is Container container) { foreach (var child in container.OfType().OfType()) diff --git a/osu.Game/Screens/Play/SongProgress.cs b/osu.Game/Screens/Play/SongProgress.cs index cab44c7473..bd861dc598 100644 --- a/osu.Game/Screens/Play/SongProgress.cs +++ b/osu.Game/Screens/Play/SongProgress.cs @@ -78,6 +78,8 @@ namespace osu.Game.Screens.Play private IClock referenceClock; + public bool UsesFixedAnchor { get; set; } + public SongProgress() { RelativeSizeAxes = Axes.X; diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index 9cca0ba2c7..23f36ffe5b 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -149,13 +149,21 @@ namespace osu.Game.Skinning.Editor { foreach (var c in SelectedBlueprints) { - Drawable drawable = (Drawable)c.Item; + var item = c.Item; + Drawable drawable = (Drawable)item; + drawable.Position += drawable.ScreenSpaceDeltaToParentSpace(moveEvent.ScreenSpaceDelta); + + if (item.UsesFixedAnchor) continue; + + applyClosestAnchor(drawable); } return true; } + private static void applyClosestAnchor(Drawable drawable) => applyAnchor(drawable, getClosestAnchor(drawable)); + protected override void OnSelectionChanged() { base.OnSelectionChanged(); @@ -171,20 +179,27 @@ namespace osu.Game.Skinning.Editor protected override IEnumerable GetContextMenuItemsForSelection(IEnumerable> selection) { + var closestItem = new TernaryStateRadioMenuItem("Closest", MenuItemType.Standard, _ => applyClosestAnchors()) + { + State = { Value = GetStateFromSelection(selection, c => !c.Item.UsesFixedAnchor) } + }; + yield return new OsuMenuItem("Anchor") { - Items = createAnchorItems(d => d.Anchor, applyAnchor).ToArray() + Items = createAnchorItems((d, a) => d.UsesFixedAnchor && ((Drawable)d).Anchor == a, applyFixedAnchors) + .Prepend(closestItem) + .ToArray() }; yield return new OsuMenuItem("Origin") { - Items = createAnchorItems(d => d.Origin, applyOrigin).ToArray() + Items = createAnchorItems((d, o) => ((Drawable)d).Origin == o, applyOrigins).ToArray() }; foreach (var item in base.GetContextMenuItemsForSelection(selection)) yield return item; - IEnumerable createAnchorItems(Func checkFunction, Action applyFunction) + IEnumerable createAnchorItems(Func checkFunction, Action applyFunction) { var displayableAnchors = new[] { @@ -198,12 +213,11 @@ namespace osu.Game.Skinning.Editor Anchor.BottomCentre, Anchor.BottomRight, }; - return displayableAnchors.Select(a => { return new TernaryStateRadioMenuItem(a.ToString(), MenuItemType.Standard, _ => applyFunction(a)) { - State = { Value = GetStateFromSelection(selection, c => checkFunction((Drawable)c.Item) == a) } + State = { Value = GetStateFromSelection(selection, c => checkFunction(c.Item, a)) } }; }); } @@ -215,15 +229,21 @@ namespace osu.Game.Skinning.Editor drawable.Parent.ToLocalSpace(screenSpacePosition) - drawable.AnchorPosition; } - private void applyOrigin(Anchor anchor) + private void applyOrigins(Anchor origin) { foreach (var item in SelectedItems) { var drawable = (Drawable)item; + if (origin == drawable.Origin) continue; + var previousOrigin = drawable.OriginPosition; - drawable.Origin = anchor; + drawable.Origin = origin; drawable.Position += drawable.OriginPosition - previousOrigin; + + if (item.UsesFixedAnchor) continue; + + applyClosestAnchor(drawable); } } @@ -234,18 +254,86 @@ namespace osu.Game.Skinning.Editor private Quad getSelectionQuad() => GetSurroundingQuad(SelectedBlueprints.SelectMany(b => b.Item.ScreenSpaceDrawQuad.GetVertices().ToArray())); - private void applyAnchor(Anchor anchor) + private void applyFixedAnchors(Anchor anchor) { foreach (var item in SelectedItems) { var drawable = (Drawable)item; - var previousAnchor = drawable.AnchorPosition; - drawable.Anchor = anchor; - drawable.Position -= drawable.AnchorPosition - previousAnchor; + item.UsesFixedAnchor = true; + applyAnchor(drawable, anchor); } } + private void applyClosestAnchors() + { + foreach (var item in SelectedItems) + { + item.UsesFixedAnchor = false; + applyClosestAnchor((Drawable)item); + } + } + + private static Anchor getClosestAnchor(Drawable drawable) + { + var parent = drawable.Parent; + + if (parent == null) + return drawable.Anchor; + + var screenPosition = getScreenPosition(); + + var absolutePosition = parent.ToLocalSpace(screenPosition); + var factor = parent.RelativeToAbsoluteFactor; + + var result = default(Anchor); + + static Anchor getAnchorFromPosition(float xOrY, Anchor anchor0, Anchor anchor1, Anchor anchor2) + { + if (xOrY >= 2 / 3f) + return anchor2; + + if (xOrY >= 1 / 3f) + return anchor1; + + return anchor0; + } + + result |= getAnchorFromPosition(absolutePosition.X / factor.X, Anchor.x0, Anchor.x1, Anchor.x2); + result |= getAnchorFromPosition(absolutePosition.Y / factor.Y, Anchor.y0, Anchor.y1, Anchor.y2); + + return result; + + Vector2 getScreenPosition() + { + var quad = drawable.ScreenSpaceDrawQuad; + var origin = drawable.Origin; + + var pos = quad.TopLeft; + + if (origin.HasFlagFast(Anchor.x2)) + pos.X += quad.Width; + else if (origin.HasFlagFast(Anchor.x1)) + pos.X += quad.Width / 2f; + + if (origin.HasFlagFast(Anchor.y2)) + pos.Y += quad.Height; + else if (origin.HasFlagFast(Anchor.y1)) + pos.Y += quad.Height / 2f; + + return pos; + } + } + + private static void applyAnchor(Drawable drawable, Anchor anchor) + { + if (anchor == drawable.Anchor) return; + + var previousAnchor = drawable.AnchorPosition; + drawable.Anchor = anchor; + drawable.Position -= drawable.AnchorPosition - previousAnchor; + } + private static void adjustScaleFromAnchor(ref Vector2 scale, Anchor reference) { // cancel out scale in axes we don't care about (based on which drag handle was used). diff --git a/osu.Game/Skinning/ISkinnableDrawable.cs b/osu.Game/Skinning/ISkinnableDrawable.cs index d42b6f71b0..60b40982e5 100644 --- a/osu.Game/Skinning/ISkinnableDrawable.cs +++ b/osu.Game/Skinning/ISkinnableDrawable.cs @@ -14,5 +14,12 @@ namespace osu.Game.Skinning /// Whether this component should be editable by an end user. /// bool IsEditable => true; + + /// + /// In the context of the skin layout editor, whether this has a permanent anchor defined. + /// If , this 's is automatically determined by proximity, + /// If , a fixed anchor point has been defined. + /// + bool UsesFixedAnchor { get; set; } } } diff --git a/osu.Game/Skinning/LegacyAccuracyCounter.cs b/osu.Game/Skinning/LegacyAccuracyCounter.cs index 16562d9571..fd5a9500d9 100644 --- a/osu.Game/Skinning/LegacyAccuracyCounter.cs +++ b/osu.Game/Skinning/LegacyAccuracyCounter.cs @@ -12,6 +12,8 @@ namespace osu.Game.Skinning { public class LegacyAccuracyCounter : GameplayAccuracyCounter, ISkinnableDrawable { + public bool UsesFixedAnchor { get; set; } + public LegacyAccuracyCounter() { Anchor = Anchor.TopRight; diff --git a/osu.Game/Skinning/LegacyHealthDisplay.cs b/osu.Game/Skinning/LegacyHealthDisplay.cs index 1da80f6613..67280e4acd 100644 --- a/osu.Game/Skinning/LegacyHealthDisplay.cs +++ b/osu.Game/Skinning/LegacyHealthDisplay.cs @@ -27,6 +27,8 @@ namespace osu.Game.Skinning private bool isNewStyle; + public bool UsesFixedAnchor { get; set; } + [BackgroundDependencyLoader] private void load(ISkinSource source) { diff --git a/osu.Game/Skinning/LegacyScoreCounter.cs b/osu.Game/Skinning/LegacyScoreCounter.cs index 64ea03d59c..a12defe87e 100644 --- a/osu.Game/Skinning/LegacyScoreCounter.cs +++ b/osu.Game/Skinning/LegacyScoreCounter.cs @@ -13,6 +13,8 @@ namespace osu.Game.Skinning protected override double RollingDuration => 1000; protected override Easing RollingEasing => Easing.Out; + public bool UsesFixedAnchor { get; set; } + public LegacyScoreCounter() : base(6) { diff --git a/osu.Game/Skinning/SkinnableTargetComponentsContainer.cs b/osu.Game/Skinning/SkinnableTargetComponentsContainer.cs index 2107ca7a8b..67114de948 100644 --- a/osu.Game/Skinning/SkinnableTargetComponentsContainer.cs +++ b/osu.Game/Skinning/SkinnableTargetComponentsContainer.cs @@ -17,6 +17,8 @@ namespace osu.Game.Skinning { public bool IsEditable => false; + public bool UsesFixedAnchor { get; set; } + private readonly Action applyDefaults; ///