Skip to main content

cranpose_liquid/widgets/
language_picker.rs

1use cranpose_core::rememberMutableStateOf;
2use cranpose_macros::composable;
3use cranpose_ui::{
4    LanguagePreference, LinearArrangement, LocalizationController, Modifier,
5    text::TextStyle,
6    widgets::{Column, ColumnSpec, FlowRow, FlowRowSpec, Text},
7};
8
9use super::{
10    button::{GlassButton, GlassButtonLabel, GlassButtonSize, GlassButtonSpec},
11    card::LiquidCard,
12};
13use crate::theme::{liquid_colors, liquid_typography};
14
15/// A language chooser for an application that uses Cranpose localization.
16///
17/// The app's catalog supplies the offered languages and their native names.
18/// The controller saves a selection, updates the app's translator, and exposes
19/// any storage error for this widget to display. Enable the `localization`
20/// feature on `cranpose-liquid` to use this component.
21#[composable]
22pub fn LiquidLanguagePicker(controller: LocalizationController) {
23    let expanded = rememberMutableStateOf(|| false);
24    let preference = controller.preference();
25    let current_name = preference_name(&preference);
26    let colors = liquid_colors();
27    let typography = liquid_typography();
28    let title = cranpose_ui::tr!("App language", id = "app-language");
29    let change = cranpose_ui::tr!("Change app language", id = "change-language");
30    let system_name = cranpose_ui::tr!("System default", id = "system-language");
31
32    LiquidCard(Modifier::empty().fill_max_width(), move || {
33        let preference = preference.clone();
34        let current_name = current_name.clone();
35        let title = title.clone();
36        let change = change.clone();
37        let system_name = system_name.clone();
38        let controller = controller.clone();
39        let expanded = expanded;
40        let typography = typography.clone();
41        let colors = colors;
42        Column(
43            Modifier::empty().padding(16.0),
44            ColumnSpec::default().vertical_arrangement(LinearArrangement::spaced_by(8.0)),
45            move || {
46                let mut heading_style = typography.headline.clone();
47                heading_style.span_style.color = Some(colors.label);
48                Text(title.clone(), Modifier::empty(), heading_style);
49                Text(
50                    current_name.clone(),
51                    Modifier::empty(),
52                    typography.body.clone(),
53                );
54
55                let state_name = current_name.as_str().to_owned();
56                let toggle_modifier =
57                    Modifier::empty()
58                        .fill_max_width()
59                        .semantics(move |semantics| {
60                            semantics.state_description = Some(state_name.clone());
61                        });
62                let toggle = expanded;
63                let change_label = change.clone();
64                let change_spec = GlassButtonSpec::glass();
65                GlassButton(
66                    toggle_modifier,
67                    change_spec.clone(),
68                    move || toggle.set(!toggle.get()),
69                    move || GlassButtonLabel(change_label.clone(), change_spec.clone()),
70                );
71
72                if expanded.get() {
73                    let preference = preference.clone();
74                    let controller = controller.clone();
75                    let system_name = system_name.clone();
76                    FlowRow(
77                        Modifier::empty().fill_max_width(),
78                        FlowRowSpec::new()
79                            .main_axis_spacing(8.0)
80                            .cross_axis_spacing(8.0),
81                        move || {
82                            let system_selected = preference == LanguagePreference::System;
83                            let system_controller = controller.clone();
84                            let system_expanded = expanded;
85                            add_language_choice(system_name.clone(), system_selected, move || {
86                                system_controller.select(LanguagePreference::System);
87                                system_expanded.set(false);
88                            });
89
90                            for language in controller.catalog().languages() {
91                                let selected =
92                                    preference == LanguagePreference::Selected(language.clone());
93                                let choice = language.clone();
94                                let label = language.name().to_owned().into();
95                                let choice_controller = controller.clone();
96                                let choice_expanded = expanded;
97                                add_language_choice(label, selected, move || {
98                                    choice_controller
99                                        .select(LanguagePreference::Selected(choice.clone()));
100                                    choice_expanded.set(false);
101                                });
102                            }
103                        },
104                    );
105                }
106
107                if let Some(error) = controller.error() {
108                    let message = cranpose_ui::tr!(
109                        "Could not save language: {error}",
110                        error = error,
111                        id = "m-bae8fe9c44aa9004"
112                    );
113                    let mut error_style: TextStyle = typography.body.clone();
114                    error_style.span_style.color = Some(colors.destructive);
115                    Text(message, Modifier::empty(), error_style);
116                }
117            },
118        );
119    });
120}
121
122fn preference_name(preference: &LanguagePreference) -> cranpose_ui::SharedText {
123    match preference {
124        LanguagePreference::System => cranpose_ui::tr!("System default", id = "system-language"),
125        LanguagePreference::Selected(language) => language.name().to_owned().into(),
126    }
127}
128
129fn add_language_choice(
130    label: cranpose_ui::SharedText,
131    selected: bool,
132    on_click: impl Fn() + 'static,
133) {
134    let modifier =
135        Modifier::empty().semantics(move |semantics| semantics.selected = Some(selected));
136    let spec = GlassButtonSpec::plain().with_size(GlassButtonSize::Small);
137    GlassButton(modifier, spec.clone(), on_click, move || {
138        GlassButtonLabel(label.clone(), spec.clone());
139    });
140}