Popup UI for choosing a country from the player catalog. Shows searchable options, Russian/localized names, keyboard navigation and a checkmark for the current selection.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
using Sandbox.UI;
using Socer;
// Keep canonical catalogue keys in ClubHud; translate only the displayed names.
public static class CatalogCountryNames
{
public const string All="All countries";
public static string Russian(string country)=>country=="Croatia"?"Хорватия":UiRussian.Translate(country);
public static string Display(string country)=>country==All?
GamePreferences.Russian?"Все страны":All:GamePreferences.Russian?Russian(country):country;
public static bool Matches(string country,string query)=>country.Contains(query,StringComparison.OrdinalIgnoreCase) ||
Russian(country).Contains(query,StringComparison.OrdinalIgnoreCase);
public static string Key(string country)=>country==All?"all":country.ToLowerInvariant().Replace(' ','-');
}
// Engine popups own click-away, source visibility, focus and viewport clamping.
// The search entry consumes only navigation keys; ordinary typing, IME and
// clipboard editing retain TextEntry's native behaviour.
[StyleSheet("/CatalogCountryPicker.razor.scss")]
public sealed class CatalogCountryPopup : Popup
{
readonly CatalogCountrySearch search;
readonly Panel options;
readonly Label empty;
readonly Action<string> choose;
readonly string selected;
readonly string[] countries;
readonly List<(string Country,Button Button)> rows=new();
int keyboardRow;
bool focusSearch=true;
int preferencesRevision;
public string Query=>search.Text??"";
public IReadOnlyList<string> VisibleCountries=>rows.Select(row=>row.Country).ToArray();
public string KeyboardCountry=>rows.Count>0?rows[Math.Clamp(keyboardRow,0,rows.Count-1)].Country:CatalogCountryNames.All;
public CatalogCountryPopup(Panel source,string selected,Action<string> choose)
:base(source,PositionMode.BelowLeft,4f)
{
this.selected=selected;this.choose=choose;
AddClass("catalog-country-popup localized-text");CloseWhenParentIsHidden=true;
preferencesRevision=GamePreferences.Revision;
countries=PlayerCatalog.CardVariants.Select(card=>card.Country).Distinct(StringComparer.Ordinal)
.OrderBy(CatalogCountryNames.Display,StringComparer.CurrentCulture).ToArray();
var heading=Add.Panel("country-picker-heading");
heading.AddChild(new Label{Text=GamePreferences.Russian?"Выберите страну":"Choose a country",Classes="country-picker-title"});
var close=heading.AddChild<Button>();close.AddClass("country-picker-close");close.Text="×";
close.AddEventListener("onclick",()=>Delete());
search=AddChild<CatalogCountrySearch>();search.AddClass("catalog-country-search");
search.Placeholder=GamePreferences.Russian?"Поиск страны…":"Search countries…";
search.Navigate=HandleKey;search.AddEventListener("value.changed",UpdateResults);
options=Add.Panel("catalog-country-options");
empty=AddChild(new Label{Classes="catalog-country-empty",Text=GamePreferences.Russian?"Страна не найдена":"No countries found"});
UpdateResults();
}
void UpdateResults()
{
options.DeleteChildren(true);rows.Clear();
var query=Query.Trim();
AddCountry(CatalogCountryNames.All);
foreach(var country in countries.Where(country=>query.Length==0 || CatalogCountryNames.Matches(country,query)))AddCountry(country);
empty.SetClass("hidden",rows.Count>1);
keyboardRow=query.Length>0 && rows.Count>1?1:Math.Max(0,rows.FindIndex(row=>row.Country==selected));
ShowKeyboardRow(false);
}
void AddCountry(string country)
{
var button=options.AddChild<Button>();
button.AddClass("catalog-country-option country-option-"+CatalogCountryNames.Key(country));
button.SetClass("chosen",country==selected);button.FocusOnClick=false;
button.Text=CatalogCountryNames.Display(country);
button.AddEventListener("onclick",()=>Select(country));
if(country==selected)button.AddChild(new Label{Classes="country-option-check",Text="✓"});
rows.Add((country,button));
}
void Select(string country){choose?.Invoke(country);if(!IsDeleting)Delete();}
void ShowKeyboardRow(bool scroll)
{
for(int i=0;i<rows.Count;i++)rows[i].Button.SetClass("keyboard-current",i==keyboardRow);
if(scroll && rows.Count>0)
{
var row=rows[keyboardRow].Button;
var top=row.PanelPositionToScreenPosition(Vector2.Zero).y;
var bottom=row.PanelPositionToScreenPosition(new Vector2(0,row.Box.Rect.Height)).y;
var listTop=options.PanelPositionToScreenPosition(Vector2.Zero).y;
var listBottom=options.PanelPositionToScreenPosition(new Vector2(0,options.Box.Rect.Height)).y;
float offset=top<listTop?top-listTop:bottom>listBottom?bottom-listBottom:0;
if(Math.Abs(offset)>.5f)options.ScrollTo(new Vector2(0,Math.Max(0,options.ScrollOffset.y+offset)));
}
}
bool HandleKey(string key)
{
if(key=="escape"){Delete();return true;}
if(key is "up" or "uparrow" or "arrowup" or "down" or "downarrow" or "arrowdown")
{
bool up=key is "up" or "uparrow" or "arrowup";
keyboardRow=Math.Clamp(keyboardRow+(up?-1:1),0,Math.Max(0,rows.Count-1));ShowKeyboardRow(true);return true;
}
if((key is "enter" or "return") && rows.Count>0){Select(KeyboardCountry);return true;}
return false;
}
public override void OnButtonTyped(ButtonEvent e)
{
if(e.Button=="escape"){Delete();e.StopPropagation=true;return;}
base.OnButtonTyped(e);
}
public override void Tick()
{
base.Tick();
if(IsDeleting)return;
if(preferencesRevision!=GamePreferences.Revision){Delete();return;}
if(focusSearch && search.Box.Rect.Width>0){focusSearch=false;search.Focus();ShowKeyboardRow(true);}
}
}
public sealed class CatalogCountrySearch : TextEntry
{
public Func<string,bool> Navigate;
public override void OnButtonTyped(ButtonEvent e)
{
if(Navigate?.Invoke(e.Button)==true){e.StopPropagation=true;return;}
base.OnButtonTyped(e);
}
}