super-tts-app/src/ui/views/language_picker.rs
Line | Count | Source |
1 | | // SPDX-License-Identifier: GPL-3.0-only |
2 | | use cosmic::Element; |
3 | | use cosmic::iced::Length; |
4 | | use cosmic::widget::{self, button, column, scrollable, text_input}; |
5 | | |
6 | | use crate::core::app::AppModel; |
7 | | use crate::ui::languages::friendly_name; |
8 | | use crate::ui::messages::{LanguageMessage, Message}; |
9 | | |
10 | | /// The language search sheet, in either of its two modes. |
11 | | /// |
12 | | /// Both lists are the daemon's, never this app's. Which tags a model answers to |
13 | | /// — whether it wants `en` or `en-US` — is a rule only the daemon's resolver |
14 | | /// knows, and the global setting's vocabulary is the union of what the installed |
15 | | /// models can speak, so it grows and shrinks as backends come and go. The |
16 | | /// hard-coded table that used to fill the global half offered tags no model |
17 | | /// served and, more quietly, never learned about the ones a freshly installed |
18 | | /// backend added. `auto` is pinned rather than taken from either list, because |
19 | | /// it is a control and not a language, and a search query must not be able to |
20 | | /// filter it away. |
21 | 0 | pub fn sheet(app: &AppModel) -> Element<'_, Message> { |
22 | 0 | let spacing = cosmic::theme::spacing(); |
23 | 0 | let q = app.language.language_picker_query.to_lowercase(); |
24 | | |
25 | | // Build the candidate (tag, label) list for the active mode. Special |
26 | | // entries stay pinned at the top; the language entries are sorted |
27 | | // alphabetically by display name. |
28 | 0 | let mut pinned: Vec<(Option<String>, String)> = Vec::new(); |
29 | 0 | let mut langs: Vec<(Option<String>, String)> = Vec::new(); |
30 | 0 | if let Some((ref src, ref mdl)) = app.language.language_picker_target { |
31 | | // Per-model sheet. |
32 | 0 | pinned.push((None, "Follow global".to_string())); // clear → DELETE |
33 | 0 | pinned.push((Some("auto".to_string()), friendly_name("auto"))); // "Auto-detect" |
34 | | // The tags this model can be pinned to — but only when the held list |
35 | | // belongs to this exact (source, model) pair (stale-list guard). An |
36 | | // empty list is a real answer for a monolingual model, and leaves the |
37 | | // sheet with nothing but the two pinned controls. |
38 | 0 | if app.language.model_language_for.as_ref() == Some(&(src.clone(), mdl.clone())) { |
39 | 0 | for tag in &app.language.model_languages { |
40 | 0 | if tag.eq_ignore_ascii_case("auto") { |
41 | 0 | continue; // already pinned as "Auto-detect" |
42 | 0 | } |
43 | 0 | langs.push((Some(tag.clone()), friendly_name(tag))); |
44 | | } |
45 | 0 | } |
46 | | } else { |
47 | | // Global sheet — "Auto-detect" only; the unset state is reached by not |
48 | | // choosing anything, so there is no explicit "No preference" entry. |
49 | 0 | pinned.push((Some("auto".to_string()), friendly_name("auto"))); // "Auto-detect" |
50 | 0 | for tag in &app.language.primary_languages { |
51 | 0 | if tag.eq_ignore_ascii_case("auto") { |
52 | 0 | continue; // the daemon always includes it; it is pinned above |
53 | 0 | } |
54 | 0 | langs.push((Some(tag.clone()), friendly_name(tag))); |
55 | | } |
56 | | } |
57 | 0 | langs.sort_by(|a, b| a.1.cmp(&b.1)); |
58 | | // The pinned controls always show; only the language list is narrowed by the |
59 | | // search query, so the user can never filter away "Auto-detect" / "Follow |
60 | | // global". |
61 | 0 | let rows: Vec<(Option<String>, String)> = pinned |
62 | 0 | .into_iter() |
63 | 0 | .chain(langs.into_iter().filter(|(tag, label)| { |
64 | 0 | if q.is_empty() { |
65 | 0 | return true; |
66 | 0 | } |
67 | 0 | let hay = format!("{label} {}", tag.as_deref().unwrap_or("")).to_lowercase(); |
68 | 0 | hay.contains(&q) |
69 | 0 | })) |
70 | 0 | .collect(); |
71 | | |
72 | | // Search field pinned at the top. |
73 | 0 | let search = text_input("Search languages…", &app.language.language_picker_query) |
74 | 0 | .on_input(|s| Message::Language(LanguageMessage::LanguagePickerQueryChanged(s))) |
75 | 0 | .width(Length::Fill); |
76 | | |
77 | 0 | let mut list = column::with_capacity(rows.len()).spacing(spacing.space_xxs); |
78 | 0 | for (tag, label) in rows { |
79 | 0 | let msg = if let Some((ref src, ref mdl)) = app.language.language_picker_target { |
80 | 0 | Message::Language(LanguageMessage::ModelLanguageSelected { |
81 | 0 | source: src.clone(), |
82 | 0 | model: mdl.clone(), |
83 | 0 | choice: tag, |
84 | 0 | }) |
85 | | } else { |
86 | 0 | Message::Language(LanguageMessage::PrimaryLanguageSelected(tag)) |
87 | | }; |
88 | 0 | list = list.push(button::text(label).width(Length::Fill).on_press(msg)); |
89 | | } |
90 | | |
91 | 0 | widget::column::with_capacity(2) |
92 | 0 | .spacing(spacing.space_s) |
93 | 0 | .push(search) |
94 | 0 | .push(scrollable(list).height(Length::Fill)) |
95 | 0 | .width(Length::Fill) |
96 | 0 | .into() |
97 | 0 | } |