Skip to content

Commit d4098ef

Browse files
committed
Added BsListControl.
1 parent a06e05f commit d4098ef

3 files changed

Lines changed: 245 additions & 259 deletions

File tree

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using HMUI;
4+
using JetBrains.Annotations;
5+
using Reactive.Compiler;
6+
using Reactive.Components;
7+
using Reactive.Yoga;
8+
using TMPro;
9+
using UnityEngine;
10+
using BsImage = Reactive.BeatSaber.Components.Image;
11+
12+
namespace Reactive.BeatSaber.Components;
13+
14+
public record struct BsListControlItem<T>(T Key, string? Text, Sprite? Icon);
15+
16+
[PublicAPI]
17+
public partial class BsListControl<T> : ReactiveComponent, ISkewedComponent {
18+
#region Public API
19+
20+
[Required]
21+
public IReadOnlyList<BsListControlItem<T>> Items {
22+
get => _items!;
23+
set {
24+
_items = value;
25+
26+
if (_initialized) {
27+
if (FindKey(Key) is var idx && idx != _selectedIdx.Value) {
28+
SetKey(idx);
29+
}
30+
} else {
31+
DoInitialUpdate();
32+
}
33+
}
34+
}
35+
36+
[Required]
37+
public T Key {
38+
get => _key!;
39+
set {
40+
if (_initialized) {
41+
SetKey(value);
42+
} else {
43+
_key = value;
44+
DoInitialUpdate();
45+
}
46+
}
47+
}
48+
49+
public float Skew {
50+
get => _skew.Value;
51+
set => _skew.Value = value;
52+
}
53+
54+
public bool Interactable {
55+
get => _interactable.Value;
56+
set => _interactable.Value = value;
57+
}
58+
59+
public Action<T>? OnKeyChanged { get; set; }
60+
61+
private bool _initialized;
62+
private IReadOnlyList<BsListControlItem<T>>? _items;
63+
private T? _key;
64+
65+
private void DoInitialUpdate() {
66+
if (!_initialized && _key != null && _items != null) {
67+
SetKey(_key);
68+
_initialized = true;
69+
}
70+
}
71+
72+
private void SetKey(T key) {
73+
var index = -1;
74+
75+
if (_items != null) {
76+
for (var i = 0; i < _items.Count; i++) {
77+
if (EqualityComparer<T>.Default.Equals(_items[i].Key, key)) {
78+
index = i;
79+
}
80+
}
81+
}
82+
83+
if (index is -1) {
84+
throw new KeyNotFoundException();
85+
}
86+
87+
SetKey(index);
88+
}
89+
90+
private void SetKey(int index) {
91+
_key = _items![index].Key;
92+
_selectedIdx.Value = index;
93+
OnKeyChanged?.Invoke(_key);
94+
}
95+
96+
private int FindKey(T key) {
97+
var index = -1;
98+
99+
if (_items == null) {
100+
return index;
101+
}
102+
103+
for (var i = 0; i < _items.Count; i++) {
104+
if (EqualityComparer<T>.Default.Equals(_items[i].Key, key)) {
105+
index = i;
106+
}
107+
}
108+
109+
return index;
110+
}
111+
112+
#endregion
113+
114+
#region Construct
115+
116+
private State<float> _skew = null!;
117+
private State<int> _selectedIdx = null!;
118+
private State<bool> _interactable = null!;
119+
120+
protected override GameObject Construct() {
121+
_skew = Remember(BeatSaberStyle.Skew);
122+
_selectedIdx = Remember(0);
123+
_interactable = Remember(true);
124+
125+
var selected = _selectedIdx.Map(x => _items?[x]);
126+
127+
var nextAvailable = RememberDerived(
128+
x => x._interactable && x._selectedIdx < _items?.Count - 1,
129+
(_selectedIdx, _interactable)
130+
);
131+
132+
var prevAvailable = RememberDerived(
133+
x => x._interactable && x._selectedIdx > 0 && _items?.Count > 1,
134+
(_selectedIdx, _interactable)
135+
);
136+
137+
return new Layout {
138+
FlexItem = {
139+
Size = new() { x = 40.pt, y = 6.pt }
140+
},
141+
142+
FlexController = {
143+
JustifyContent = Justify.Center
144+
},
145+
146+
Children = {
147+
new Label {
148+
Alignment = TextAlignmentOptions.Capline,
149+
sFontStyle = _skew.Map(x => x > 0 ? FontStyles.Italic : FontStyles.Normal),
150+
151+
sText = selected.Map(x => x?.Text).Where(x => x != null),
152+
sEnabled = selected.Map(x => x?.Text != null)
153+
}.AsFlexItem(),
154+
155+
new BsImage {
156+
FlexItem = {
157+
Margin = new() { top = 2.pt, bottom = 2.pt },
158+
AspectRatio = 1f
159+
},
160+
161+
PreserveAspect = true,
162+
sSkew = _skew,
163+
164+
sSprite = selected.Map(x => x?.Icon).Where(x => x != null),
165+
sEnabled = selected.Map(x => x?.Icon != null)
166+
},
167+
168+
CreateButton(true, () => SetKey(_selectedIdx.Value - 1), prevAvailable),
169+
CreateButton(false, () => SetKey(_selectedIdx.Value + 1), nextAvailable)
170+
}
171+
}.Use();
172+
}
173+
174+
[StateGen]
175+
private static IReactiveComponent CreateButton(bool left, Action callback, IState<bool> interactable) {
176+
var hovered = Remember(false);
177+
178+
var highlighted = RememberDerived(
179+
x => x.hovered.Value && x.interactable.Value,
180+
(hovered, interactable)
181+
);
182+
183+
var color0 = highlighted.Map(x => Color.white with { a = x ? 0.2f : 1f });
184+
var color1 = highlighted.Map(x => x ? Color.black with { a = 0.5f } : Color.white);
185+
186+
return new Background {
187+
FlexController = {
188+
Padding = 1.5f,
189+
JustifyContent = left ? Justify.FlexStart : Justify.FlexEnd
190+
},
191+
192+
FlexItem = {
193+
Size = new() { x = 50.pct, y = 100.pct },
194+
Position = left ?
195+
new() { top = 0.pt, left = 0.pt } :
196+
new() { top = 0.pt, right = 0.pt },
197+
PositionType = PositionType.Absolute
198+
},
199+
200+
sColor = highlighted.Map(x => x ? Color.white : Color.black with { a = 0.5f }),
201+
202+
sGradientColor0 = left ? color0 : color1,
203+
sGradientColor1 = left ? color1 : color0,
204+
205+
UseGradient = true,
206+
GradientDirection = ImageView.GradientDirection.Horizontal,
207+
208+
Sprite = left ? BeatSaberResources.Sprites.backgroundLeft : BeatSaberResources.Sprites.backgroundRight,
209+
Material = GameResources.UINoGlowMaterial,
210+
PixelsPerUnit = 12f,
211+
212+
Do = x => x.WithPointerEvents(
213+
onEnter: _ => hovered.Value = true,
214+
onLeave: _ => hovered.Value = false,
215+
onDown: _ => {
216+
if (interactable.Value) {
217+
callback();
218+
}
219+
}
220+
),
221+
222+
Children = {
223+
new BsImage {
224+
FlexItem = {
225+
AspectRatio = 1f
226+
},
227+
228+
Sprite = GameResources.ArrowIcon,
229+
PreserveAspect = true,
230+
231+
sColor = interactable.Map(x => x ?
232+
Color.white with { a = 0.8f } :
233+
Color.white with { a = 0.25f } * 0.9f
234+
),
235+
236+
ContentTransform = {
237+
localEulerAngles = new(0f, 0f, left ? 270f : 90f)
238+
}
239+
}
240+
}
241+
};
242+
}
243+
244+
#endregion
245+
}

0 commit comments

Comments
 (0)