Files
Ely-GPUI-Components/src/forms/catalogs.rs
T

192 lines
6.3 KiB
Rust

use std::rc::Rc;
use gpui::{
App, ElementId, Entity, IntoElement, ParentElement, RenderOnce, SharedString, Styled, Window,
div, prelude::*,
};
use iso_currency::{Currency, IntoEnumIterator};
use isocountry::CountryCode;
use super::{Choice, Combobox, TextInput, options::OnValue};
use crate::theme::{ActiveTheme, TextSize};
/// The flag emoji of a two-letter region code.
pub(crate) fn flag(region: &str) -> String {
region
.chars()
.map(|letter| {
assert!(letter.is_ascii_uppercase(), "region code {region:?}");
char::from_u32(0x1F1E6 + (letter as u32 - 'A' as u32)).expect("a regional indicator")
})
.collect()
}
/// `text` with its first letter capitalized, as list items read.
fn titled(text: &str) -> String {
let mut chars = text.chars();
chars.next().map_or_else(String::new, |first| {
first.to_uppercase().chain(chars).collect()
})
}
fn by_label(mut choices: Vec<Choice>) -> Vec<Choice> {
choices.sort_by_key(|choice| choice.label.to_lowercase());
choices
}
/// ISO 639-1 languages under their own names, with the English name and code beside.
fn languages() -> Vec<Choice> {
let choices = isolang::languages()
.filter_map(|language| {
let (code, english) = (language.to_639_1()?, language.to_name());
Some(match language.to_autonym() {
Some(own) => Choice::new(code, titled(own)).note(format!("{english} · {code}")),
None => Choice::new(code, english).note(code),
})
})
.collect();
by_label(choices)
}
impl Combobox {
/// ISO 639-1 languages under their own names; a name in either tongue or the code finds one.
pub fn languages(id: impl Into<ElementId>, state: &Entity<TextInput>) -> Self {
Self::new(id, state, languages())
}
/// ISO 3166-1 countries and territories, flagged, with their codes.
pub fn countries(id: impl Into<ElementId>, state: &Entity<TextInput>) -> Self {
let mut named: Vec<(&str, &str)> = CountryCode::iter()
.map(|country| (country.name(), country.alpha2()))
.collect();
named.sort_by_key(|(name, _)| name.to_lowercase());
let choices = named
.into_iter()
.map(|(name, code)| Choice::new(code, format!("{} {name}", flag(code))).note(code));
Self::new(id, state, choices)
}
/// ISO 4217 currencies in use, without funds, metals or retired codes.
pub fn currencies(id: impl Into<ElementId>, state: &Entity<TextInput>) -> Self {
let choices = Currency::iter()
.filter(|currency| {
!currency.is_fund() && !currency.is_special() && currency.is_superseded().is_none()
})
.map(|currency| {
Choice::new(currency.code(), currency.name().to_string()).note(currency.code())
})
.collect();
Self::new(id, state, by_label(choices))
}
}
/// The system's font families in a combobox, and a line set in the chosen one.
#[derive(IntoElement)]
pub struct FontPicker {
id: ElementId,
state: Entity<TextInput>,
selected: Option<SharedString>,
on_change: Option<OnValue>,
}
impl FontPicker {
pub fn new(id: impl Into<ElementId>, state: &Entity<TextInput>) -> Self {
Self {
id: id.into(),
state: state.clone(),
selected: None,
on_change: None,
}
}
pub fn selected(mut self, family: impl Into<SharedString>) -> Self {
self.selected = Some(family.into());
self
}
pub fn on_change(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_change = Some(Rc::new(handler));
self
}
}
impl RenderOnce for FontPicker {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let families = window.use_keyed_state((self.id.clone(), "families"), cx, |_, cx| {
let found: Vec<Choice> = cx
.text_system()
.all_font_names()
.into_iter()
.filter(|family| !family.starts_with('.'))
.map(|family| Choice::new(family.clone(), family))
.collect();
log::info!("font picker: {} families", found.len());
found
});
let theme = cx.theme();
let mut combobox = Combobox::new(
self.id.clone(),
&self.state,
families.read(cx).iter().cloned(),
);
if let Some(family) = &self.selected {
combobox = combobox.selected(family.clone());
}
if let Some(on_change) = self.on_change {
combobox = combobox.on_change(move |family, window, cx| on_change(family, window, cx));
}
div()
.flex()
.flex_col()
.gap_2()
.w_full()
.child(combobox)
.when_some(self.selected, |picker, family| {
picker.child(
div()
.font_family(family)
.text_size(theme.text_size(TextSize::Lg))
.text_color(theme.colors.fg_muted)
.child("The quick brown fox jumps over the lazy dog"),
)
})
}
}
#[cfg(test)]
mod tests {
use super::{flag, languages, titled};
use crate::forms::combobox::matching;
#[test]
fn languages_answer_to_their_code_and_english_name() {
let found = |query| {
matching(&languages(), query)
.into_iter()
.map(|choice| choice.value)
.collect::<Vec<_>>()
};
assert!(found("zh").iter().any(|code| code == "zh"));
assert!(found("chinese").iter().any(|code| code == "zh"));
assert!(found("中文").iter().any(|code| code == "zh"));
assert!(found("aymara").iter().any(|code| code == "ay"));
}
#[test]
fn flags_come_from_region_letters() {
assert_eq!(flag("CN"), "🇨🇳");
assert_eq!(flag("US"), "🇺🇸");
}
#[test]
fn titles_capitalize_the_first_letter_only() {
assert_eq!(titled("français"), "Français");
assert_eq!(titled("русский"), "Русский");
assert_eq!(titled("中文"), "中文");
assert_eq!(titled(""), "");
}
}