diff --git a/osu.Game/Graphics/Containers/SectionsContainer.cs b/osu.Game/Graphics/Containers/SectionsContainer.cs new file mode 100644 index 0000000000..5fdb5e869e --- /dev/null +++ b/osu.Game/Graphics/Containers/SectionsContainer.cs @@ -0,0 +1,171 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Graphics.Containers +{ + /// + /// A container that can scroll to each section inside it. + /// + public class SectionsContainer : Container + { + private Drawable expandableHeader, fixedHeader, footer; + public readonly ScrollContainer ScrollContainer; + private readonly Container sectionsContainer; + + public Drawable ExpandableHeader + { + get { return expandableHeader; } + set + { + if (value == expandableHeader) return; + + if (expandableHeader != null) + Remove(expandableHeader); + expandableHeader = value; + if (value == null) return; + + Add(expandableHeader); + lastKnownScroll = float.NaN; + } + } + + public Drawable FixedHeader + { + get { return fixedHeader; } + set + { + if (value == fixedHeader) return; + + if (fixedHeader != null) + Remove(fixedHeader); + fixedHeader = value; + if (value == null) return; + + Add(fixedHeader); + lastKnownScroll = float.NaN; + } + } + + public Drawable Footer + { + get { return footer; } + set + { + if (value == footer) return; + + if (footer != null) + ScrollContainer.Remove(footer); + footer = value; + if (value == null) return; + + footer.Anchor |= Anchor.y2; + footer.Origin |= Anchor.y2; + ScrollContainer.Add(footer); + lastKnownScroll = float.NaN; + } + } + + public Bindable SelectedSection { get; } = new Bindable(); + + protected virtual Container CreateScrollContentContainer() + => new FillFlowContainer + { + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Both + }; + + private List sections = new List(); + public IEnumerable Sections + { + get { return sections; } + set + { + foreach (var section in sections) + sectionsContainer.Remove(section); + + sections = value.ToList(); + if (sections.Count == 0) return; + + sectionsContainer.Add(sections); + SelectedSection.Value = sections[0]; + lastKnownScroll = float.NaN; + } + } + + private float headerHeight, footerHeight; + private readonly MarginPadding originalSectionsMargin; + private void updateSectionsMargin() + { + if (sections.Count == 0) return; + + var newMargin = originalSectionsMargin; + newMargin.Top += headerHeight; + newMargin.Bottom += footerHeight; + + sectionsContainer.Margin = newMargin; + } + + public SectionsContainer() + { + Add(ScrollContainer = new ScrollContainer() + { + RelativeSizeAxes = Axes.Both, + Masking = false, + Children = new Drawable[] { sectionsContainer = CreateScrollContentContainer() } + }); + originalSectionsMargin = sectionsContainer.Margin; + } + + private float lastKnownScroll; + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + float headerH = (ExpandableHeader?.LayoutSize.Y ?? 0) + (FixedHeader?.LayoutSize.Y ?? 0); + float footerH = Footer?.LayoutSize.Y ?? 0; + if (headerH != headerHeight || footerH != footerHeight) + { + headerHeight = headerH; + footerHeight = footerH; + updateSectionsMargin(); + } + + float currentScroll = Math.Max(0, ScrollContainer.Current); + if (currentScroll != lastKnownScroll) + { + lastKnownScroll = currentScroll; + + if (expandableHeader != null && fixedHeader != null) + { + float offset = Math.Min(expandableHeader.LayoutSize.Y, currentScroll); + + expandableHeader.Y = -offset; + fixedHeader.Y = -offset + expandableHeader.LayoutSize.Y; + } + + Drawable bestMatch = null; + float minDiff = float.MaxValue; + + foreach (var section in sections) + { + float diff = Math.Abs(ScrollContainer.GetChildPosInContent(section) - currentScroll); + if (diff < minDiff) + { + minDiff = diff; + bestMatch = section; + } + } + + if (bestMatch != null) + SelectedSection.Value = bestMatch; + } + } + } +} diff --git a/osu.Game/Overlays/Settings/SettingsHeader.cs b/osu.Game/Overlays/Settings/SettingsHeader.cs index 56018dc7d9..c554b54a87 100644 --- a/osu.Game/Overlays/Settings/SettingsHeader.cs +++ b/osu.Game/Overlays/Settings/SettingsHeader.cs @@ -1,34 +1,16 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; -using OpenTK.Graphics; namespace osu.Game.Overlays.Settings { public class SettingsHeader : Container { - public SearchTextBox SearchTextBox; - - private Box background; - - private readonly Func currentScrollOffset; - - public Action Exit; - - /// A reference to the current scroll position of the ScrollContainer we are contained within. - public SettingsHeader(Func currentScrollOffset) - { - this.currentScrollOffset = currentScrollOffset; - } - [BackgroundDependencyLoader] private void load(OsuColour colours) { @@ -37,11 +19,6 @@ namespace osu.Game.Overlays.Settings Children = new Drawable[] { - background = new Box - { - Colour = Color4.Black, - RelativeSizeAxes = Axes.Both, - }, new FillFlowContainer { AutoSizeAxes = Axes.Y, @@ -53,7 +30,8 @@ namespace osu.Game.Overlays.Settings { Text = "settings", TextSize = 40, - Margin = new MarginPadding { + Margin = new MarginPadding + { Left = SettingsOverlay.CONTENT_MARGINS, Top = Toolbar.Toolbar.TOOLTIP_HEIGHT }, @@ -63,45 +41,15 @@ namespace osu.Game.Overlays.Settings Colour = colours.Pink, Text = "Change the way osu! behaves", TextSize = 18, - Margin = new MarginPadding { + Margin = new MarginPadding + { Left = SettingsOverlay.CONTENT_MARGINS, Bottom = 30 }, }, - SearchTextBox = new SearchTextBox - { - RelativeSizeAxes = Axes.X, - Origin = Anchor.TopCentre, - Anchor = Anchor.TopCentre, - Width = 0.95f, - Margin = new MarginPadding { - Top = 20, - Bottom = 20 - }, - Exit = () => Exit(), - }, } } }; } - - protected override void UpdateAfterChildren() - { - base.UpdateAfterChildren(); - - // the point at which we will start anchoring to the top. - float anchorOffset = SearchTextBox.Y; - - float scrollPosition = currentScrollOffset(); - - // we want to anchor the search field to the top of the screen when scrolling. - Margin = new MarginPadding { Top = Math.Max(0, scrollPosition - anchorOffset) }; - - // we don't want the header to scroll when scrolling beyond the upper extent. - Y = Math.Min(0, scrollPosition); - - // we get darker as scroll progresses - background.Alpha = Math.Min(1, scrollPosition / anchorOffset) * 0.5f; - } } } \ No newline at end of file diff --git a/osu.Game/Overlays/Settings/SidebarButton.cs b/osu.Game/Overlays/Settings/SidebarButton.cs index 766c6cf7e2..b6e4ad3f5e 100644 --- a/osu.Game/Overlays/Settings/SidebarButton.cs +++ b/osu.Game/Overlays/Settings/SidebarButton.cs @@ -21,7 +21,7 @@ namespace osu.Game.Overlays.Settings private readonly Box backgroundBox; private readonly Box selectionIndicator; private readonly Container text; - public Action Action; + public Action Action; private SettingsSection section; public SettingsSection Section @@ -75,6 +75,7 @@ namespace osu.Game.Overlays.Settings { Width = Sidebar.DEFAULT_WIDTH, RelativeSizeAxes = Axes.Y, + Colour = OsuColour.Gray(0.6f), Children = new[] { headerText = new OsuSpriteText @@ -110,7 +111,7 @@ namespace osu.Game.Overlays.Settings protected override bool OnClick(InputState state) { - Action?.Invoke(); + Action?.Invoke(section); backgroundBox.FlashColour(Color4.White, 400); return true; } diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs index 71ad18e081..943545e858 100644 --- a/osu.Game/Overlays/SettingsOverlay.cs +++ b/osu.Game/Overlays/SettingsOverlay.cs @@ -7,10 +7,11 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; -using osu.Game.Overlays.Settings; -using System; -using osu.Game.Overlays.Settings.Sections; using osu.Framework.Input; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays.Settings; +using osu.Game.Overlays.Settings.Sections; namespace osu.Game.Overlays { @@ -26,18 +27,13 @@ namespace osu.Game.Overlays private const float sidebar_padding = 10; - private ScrollContainer scrollContainer; private Sidebar sidebar; private SidebarButton[] sidebarButtons; - private SettingsSection[] sections; + private SidebarButton selectedSidebarButton; - private SettingsHeader header; + private SettingsSectionsContainer sectionsContainer; - private SettingsFooter footer; - - private SearchContainer searchContainer; - - private float lastKnownScroll; + private SearchTextBox searchTextBox; public SettingsOverlay() { @@ -48,7 +44,7 @@ namespace osu.Game.Overlays [BackgroundDependencyLoader(permitNulls: true)] private void load(OsuGame game) { - sections = new SettingsSection[] + var sections = new SettingsSection[] { new GeneralSection(), new GraphicsSection(), @@ -68,27 +64,27 @@ namespace osu.Game.Overlays Colour = Color4.Black, Alpha = 0.6f, }, - scrollContainer = new ScrollContainer + sectionsContainer = new SettingsSectionsContainer { - ScrollDraggerVisible = false, RelativeSizeAxes = Axes.Y, Width = width, Margin = new MarginPadding { Left = SIDEBAR_WIDTH }, - Children = new Drawable[] + ExpandableHeader = new SettingsHeader(), + FixedHeader = searchTextBox = new SearchTextBox { - searchContainer = new SearchContainer + RelativeSizeAxes = Axes.X, + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + Width = 0.95f, + Margin = new MarginPadding { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Direction = FillDirection.Vertical, - Children = sections, + Top = 20, + Bottom = 20 }, - footer = new SettingsFooter(), - header = new SettingsHeader(() => scrollContainer.Current) - { - Exit = Hide, - }, - } + Exit = Hide, + }, + Sections = sections, + Footer = new SettingsFooter() }, sidebar = new Sidebar { @@ -96,84 +92,89 @@ namespace osu.Game.Overlays Children = sidebarButtons = sections.Select(section => new SidebarButton { - Selected = sections[0] == section, Section = section, - Action = () => scrollContainer.ScrollIntoView(section), + Action = sectionsContainer.ScrollContainer.ScrollIntoView, } ).ToArray() } }; - header.SearchTextBox.Current.ValueChanged += newValue => searchContainer.SearchTerm = newValue; + selectedSidebarButton = sidebarButtons[0]; + selectedSidebarButton.Selected = true; - scrollContainer.Padding = new MarginPadding { Top = game?.Toolbar.DrawHeight ?? 0 }; - } - - protected override void UpdateAfterChildren() - { - base.UpdateAfterChildren(); - - //we need to update these manually because we can't put the SettingsHeader inside the SearchContainer (due to its anchoring). - searchContainer.Y = header.DrawHeight; - footer.Y = searchContainer.Y + searchContainer.DrawHeight; - } - - protected override void Update() - { - base.Update(); - - float currentScroll = scrollContainer.Current; - if (currentScroll != lastKnownScroll) + sectionsContainer.SelectedSection.ValueChanged += section => { - lastKnownScroll = currentScroll; + selectedSidebarButton.Selected = false; + selectedSidebarButton = sidebarButtons.Single(b => b.Section == section); + selectedSidebarButton.Selected = true; + }; - SettingsSection bestCandidate = null; - float bestDistance = float.MaxValue; + searchTextBox.Current.ValueChanged += newValue => sectionsContainer.SearchContainer.SearchTerm = newValue; - foreach (SettingsSection section in sections) - { - float distance = Math.Abs(scrollContainer.GetChildPosInContent(section) - currentScroll); - if (distance < bestDistance) - { - bestDistance = distance; - bestCandidate = section; - } - } - - var previous = sidebarButtons.SingleOrDefault(sb => sb.Selected); - var next = sidebarButtons.SingleOrDefault(sb => sb.Section == bestCandidate); - if (previous != null) previous.Selected = false; - if (next != null) next.Selected = true; - } + sectionsContainer.Padding = new MarginPadding { Top = game?.Toolbar.DrawHeight ?? 0 }; } protected override void PopIn() { base.PopIn(); - scrollContainer.MoveToX(0, TRANSITION_LENGTH, EasingTypes.OutQuint); + sectionsContainer.MoveToX(0, TRANSITION_LENGTH, EasingTypes.OutQuint); sidebar.MoveToX(0, TRANSITION_LENGTH, EasingTypes.OutQuint); FadeTo(1, TRANSITION_LENGTH / 2); - header.SearchTextBox.HoldFocus = true; + searchTextBox.HoldFocus = true; } protected override void PopOut() { base.PopOut(); - scrollContainer.MoveToX(-width, TRANSITION_LENGTH, EasingTypes.OutQuint); + sectionsContainer.MoveToX(-width, TRANSITION_LENGTH, EasingTypes.OutQuint); sidebar.MoveToX(-SIDEBAR_WIDTH, TRANSITION_LENGTH, EasingTypes.OutQuint); FadeTo(0, TRANSITION_LENGTH / 2); - header.SearchTextBox.HoldFocus = false; - header.SearchTextBox.TriggerFocusLost(); + searchTextBox.HoldFocus = false; + searchTextBox.TriggerFocusLost(); } protected override bool OnFocus(InputState state) { - header.SearchTextBox.TriggerFocus(state); + searchTextBox.TriggerFocus(state); return false; } + + private class SettingsSectionsContainer : SectionsContainer + { + public SearchContainer SearchContainer; + private readonly Box headerBackground; + + protected override Container CreateScrollContentContainer() + => SearchContainer = new SearchContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Direction = FillDirection.Vertical, + }; + + public SettingsSectionsContainer() + { + ScrollContainer.ScrollDraggerVisible = false; + Add(headerBackground = new Box + { + Colour = Color4.Black, + RelativeSizeAxes = Axes.X + }); + } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + // no null check because the usage of this class is strict + headerBackground.Height = ExpandableHeader.LayoutSize.Y + FixedHeader.LayoutSize.Y; + headerBackground.Y = ExpandableHeader.Y; + headerBackground.Alpha = -ExpandableHeader.Y / ExpandableHeader.LayoutSize.Y * 0.5f; + } + } } } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 1631311ef6..99a5245bbc 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -81,6 +81,7 @@ +