prehnite/app/window/setting_window/
mod.rs1use 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}