CatalogCountryPopup.cs

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.

Rough Code
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);
    }
}