Coverage Report

Created: 2026-11-04 02:14

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
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
}