prehnite/app/window/setting_window/
mod.rs

1use crate::app::window::{Window, WindowMessage};
2use crate::widget::styles::container::{focusable, not_focused_rect_box};
3use iced::alignment::{Horizontal, Vertical};
4use iced::widget::pane_grid::Axis;
5use iced::widget::{
6    button, pane_grid, pick_list, scrollable, space, text_input, Container, MouseArea,
7};
8use iced::window::{Id, Settings};
9use iced::{widget, Element, Length, Task};
10use prehnite_core::db::Database;
11use prehnite_core::font::get_global_font_list;
12use prehnite_core::i18n::{i18n, i18n_w};
13use prehnite_core::settings::registry::SettingRegistry;
14use prehnite_core::settings::value::SettingValueType;
15use prehnite_core::settings::{GlobalSettingKey, SettingCategory, SettingEntry, SettingKey};
16use prehnite_core::widget::font::{ftext, get_font};
17use std::collections::{HashMap, HashSet};
18use tracing::error;
19use tracing_unwrap::ResultExt;
20
21impl From<SettingWindowMessage> for WindowMessage {
22    fn from(value: SettingWindowMessage) -> Self {
23        WindowMessage::SettingWindowMessage(value)
24    }
25}
26
27#[derive(Clone, Debug)]
28pub enum SettingWindowMessage {
29    CategorySelected(usize),
30    ValueChanged(SettingKey, SettingValueType),
31    Apply,
32    EntrySearchTextChanged(String),
33    None,
34}
35
36#[derive(Debug)]
37enum SettingWindowPane {
38    Editor,
39    List,
40}
41
42#[derive(Debug)]
43pub struct SettingWindow {
44    window_id: Option<Id>,
45    current_category_idx: usize,
46    pane_state: pane_grid::State<SettingWindowPane>,
47    values: HashMap<SettingKey, SettingValueType>,
48    changed: HashSet<SettingKey>,
49    entry_search_text: String,
50}
51
52impl SettingWindow {
53    fn setting_list_pane(&self) -> Element<'_, WindowMessage> {
54        iced::widget::column(
55            SettingRegistry::get_categories()
56                .iter()
57                .enumerate()
58                .filter(|(_, c)| c.category_name().contains(self.entry_search_text.as_str()))
59                .map(|(i, v)| self.category_row(i, v)),
60        )
61        .height(Length::Fill)
62        .into()
63    }
64
65    fn setting_edit_pane(&self) -> Element<'_, WindowMessage> {
66        iced::widget::column(
67            match SettingRegistry::get_categories().get(self.current_category_idx) {
68                None => return i18n_w("unknown").into(),
69                Some(v) => v
70                    .entries()
71                    .iter()
72                    .filter(|v| v.get_is_visible())
73                    .map(|v| self.setting_edit_row(v)),
74            },
75        )
76        .into()
77    }
78
79    fn is_entry_enabled(key: SettingKey) -> bool {
80        match key {
81            SettingKey::Global(_) => true,
82            SettingKey::Book(_) => Database::is_book_opened(),
83        }
84    }
85
86    fn category_row(&self, id: usize, cate: &SettingCategory) -> Element<'_, WindowMessage> {
87        MouseArea::new(
88            Container::new(ftext(cate.category_name()))
89                .padding(5)
90                .width(Length::Fill)
91                .style(focusable(id == self.current_category_idx)),
92        )
93        .on_press(SettingWindowMessage::CategorySelected(id).into())
94        .into()
95    }
96
97    fn setting_edit_row(&self, entry: &SettingEntry) -> Element<'_, WindowMessage> {
98        let key = entry.get_setting_key();
99        let input: Element<'_, WindowMessage> = match entry.default_value() {
100            SettingValueType::Bool(_) => widget::toggler(
101                self.values
102                    .get(&key)
103                    .cloned()
104                    .unwrap_or(entry.default_value().clone())
105                    .get()
106                    .unwrap_or_default(),
107            )
108            .on_toggle_maybe(
109                Some(move |v: bool| SettingWindowMessage::ValueChanged(key, v.into()).into())
110                    .filter(|_| Self::is_entry_enabled(key)),
111            )
112            .into(),
113            SettingValueType::Int(_) => iced_aw::widget::number_input(
114                &self
115                    .values
116                    .get(&key)
117                    .cloned()
118                    .unwrap_or(entry.default_value().clone())
119                    .get()
120                    .unwrap_or_default(),
121                i64::MIN..i64::MAX,
122                |_| SettingWindowMessage::None.into(),
123            )
124            .on_input_maybe(
125                Some(move |v: i64| SettingWindowMessage::ValueChanged(key, v.into()).into())
126                    .filter(|_| Self::is_entry_enabled(key)),
127            )
128            .font(get_font())
129            .into(),
130            SettingValueType::Float(_) => iced_aw::widget::number_input(
131                &self
132                    .values
133                    .get(&key)
134                    .cloned()
135                    .unwrap_or(entry.default_value().clone())
136                    .get()
137                    .unwrap_or_default(),
138                f64::MIN..f64::MAX,
139                |_| SettingWindowMessage::None.into(),
140            )
141            .on_input_maybe(
142                Some(move |v: f64| SettingWindowMessage::ValueChanged(key, v.into()).into())
143                    .filter(|_| Self::is_entry_enabled(key)),
144            )
145            .font(get_font())
146            .into(),
147            SettingValueType::String(_) => match entry.get_selectable_values() {
148                None => text_input(
149                    "value",
150                    &self
151                        .values
152                        .get(&key)
153                        .cloned()
154                        .unwrap_or(entry.default_value().clone())
155                        .get::<String>()
156                        .unwrap_or_default(),
157                )
158                .on_input_maybe(
159                    Some(move |v: String| SettingWindowMessage::ValueChanged(key, v.into()).into())
160                        .filter(|_| Self::is_entry_enabled(key)),
161                )
162                .font(get_font())
163                .into(),
164                Some(v) => {
165                    if Self::is_entry_enabled(key) {
166                        if key == GlobalSettingKey::Font.into() {
167                            pick_list(
168                                get_global_font_list().as_slice(),
169                                self.values
170                                    .get(&key)
171                                    .cloned()
172                                    .unwrap_or(entry.default_value().clone())
173                                    .get::<String>(),
174                                move |v| SettingWindowMessage::ValueChanged(key, v.into()).into(),
175                            )
176                            .font(get_font())
177                            .into()
178                        } else {
179                            pick_list(
180                                v,
181                                self.values
182                                    .get(&key)
183                                    .cloned()
184                                    .unwrap_or(entry.default_value().clone())
185                                    .get::<String>(),
186                                move |v| SettingWindowMessage::ValueChanged(key, v.into()).into(),
187                            )
188                            .font(get_font())
189                            .into()
190                        }
191                    } else {
192                        ftext("-").into()
193                    }
194                }
195            },
196        };
197        Container::new(widget::row![
198            ftext(entry.to_string())
199                .height(Length::Fill)
200                .align_y(Vertical::Center),
201            space().width(Length::Fill),
202            input
203        ])
204        .width(Length::Fill)
205        .height(40)
206        .padding(5)
207        .into()
208    }
209}
210
211impl Window for SettingWindow {
212    fn new() -> SettingWindow
213    where
214        Self: Sized,
215    {
216        let (mut pane_state, pane) = pane_grid::State::new(SettingWindowPane::List);
217        match pane_state.split(Axis::Vertical, pane, SettingWindowPane::Editor) {
218            None => {}
219            Some((_, split)) => pane_state.resize(split, 0.33),
220        }
221        Self {
222            window_id: None,
223            current_category_idx: 0,
224            pane_state,
225            values: SettingRegistry::get_values(),
226            changed: Default::default(),
227            entry_search_text: "".to_string(),
228        }
229    }
230
231    fn update(&mut self, message: WindowMessage) -> Task<WindowMessage> {
232        if let WindowMessage::SettingWindowMessage(message) = message {
233            match message {
234                SettingWindowMessage::CategorySelected(id) => {
235                    self.current_category_idx = id;
236                }
237                SettingWindowMessage::ValueChanged(key, v) => {
238                    self.changed.insert(key);
239                    self.values.insert(key, v);
240                }
241                SettingWindowMessage::Apply => {
242                    let values: Vec<(SettingKey, SettingValueType)> = self
243                        .values
244                        .clone()
245                        .into_iter()
246                        .filter(|(k, _)| self.changed.contains(k))
247                        .collect();
248                    return Task::future(async move {
249                        for (k, v) in values {
250                            SettingRegistry::immediate_apply(k, v).await.ok_or_log();
251                        }
252                    })
253                    .discard()
254                    .chain(if self.changed.contains(&GlobalSettingKey::Font.into()) {
255                        Task::done(WindowMessage::ReloadFont)
256                    } else {
257                        Task::none()
258                    })
259                    .chain(
260                        if self.changed.contains(&GlobalSettingKey::Locale.into()) {
261                            Task::done(WindowMessage::ReloadLanguage)
262                        } else {
263                            Task::none()
264                        },
265                    );
266                }
267                SettingWindowMessage::EntrySearchTextChanged(v) => {
268                    self.entry_search_text = v;
269                }
270                SettingWindowMessage::None => {}
271            }
272        } else {
273            error!("Invalid message received.");
274        }
275        Task::none()
276    }
277
278    fn view(&'_ self) -> Element<'_, WindowMessage> {
279        widget::column![
280            widget::pane_grid(&self.pane_state, |_, state, _| {
281                pane_grid::Content::new(
282                    scrollable(match state {
283                        SettingWindowPane::List => Container::new(widget::column![
284                            Container::new(
285                                text_input(
286                                    i18n("search").as_str(),
287                                    self.entry_search_text.as_str()
288                                )
289                                .font(get_font())
290                                .on_input(|v| {
291                                    SettingWindowMessage::EntrySearchTextChanged(v).into()
292                                })
293                            )
294                            .width(Length::Fill)
295                            .padding(5),
296                            self.setting_list_pane()
297                        ])
298                        .style(not_focused_rect_box),
299                        SettingWindowPane::Editor => Container::new(widget::column![
300                            Container::new(ftext(
301                                SettingRegistry::get_categories()
302                                    .get(self.current_category_idx)
303                                    .map(|v| v.category_name())
304                                    .unwrap_or(" ".to_string())
305                            ))
306                            .width(Length::Fill)
307                            .padding(5),
308                            self.setting_edit_pane()
309                        ])
310                        .style(not_focused_rect_box),
311                    })
312                    .spacing(1),
313                )
314            }),
315            Container::new(widget::row![
316                button(i18n_w("apply"))
317                    .style(button::text)
318                    .on_press(SettingWindowMessage::Apply.into())
319            ])
320            .align_x(Horizontal::Right)
321            .width(Length::Fill)
322            .padding(5)
323        ]
324        .into()
325    }
326
327    fn title(&'_ self) -> String {
328        i18n("settings")
329    }
330
331    fn set_window_id(&mut self, window_id: Id) {
332        self.window_id = Some(window_id)
333    }
334
335    fn window_settings() -> Settings
336    where
337        Self: Sized,
338    {
339        Settings {
340            size: (720f32, 560f32).into(),
341            minimizable: false,
342            resizable: false,
343            ..Settings::default()
344        }
345    }
346}