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