Skip to main content

atmos/apps/settings/
mod.rs

1//! OS 設定アプリ。
2//!
3//! テーマ(配色 JSON、`colorset/`)・キーボードレイアウト・フォントフォールバック順・
4//! キーリピート設定・WiFi 接続(SSID/パスワード)等を [`crate::kernel::config`] 経由で
5//! 読み書きする。設定変更は即時 SylFS へ永続化される。
6// src/settings.rs - OS Settings App for AtmOS
7#![allow(dead_code)]
8
9extern crate alloc;
10use alloc::string::String;
11use alloc::string::ToString;
12
13use crate::kernel::config::{self, KeyboardLayout};
14use crate::kernel::draw::{Color, Screen};
15use crate::kernel::text_box::TextBox;
16use crate::kernel::window_mgr::App;
17use crate::os_lib::json::{self};
18
19static ICON_APPEARANCE: &[u8] = include_bytes!("../../../icons/ic_fluent_color_20_regular.svg");
20static ICON_KEYBOARD: &[u8] = include_bytes!("../../../icons/ic_fluent_keyboard_20_regular.svg");
21static ICON_SYSTEM: &[u8] = include_bytes!("../../../icons/ic_fluent_settings_20_regular.svg");
22static ICON_NETWORK: &[u8] = include_bytes!("../../../icons/ic_fluent_settings_20_regular.svg");
23
24pub struct SettingsApp {
25    selected_layout: KeyboardLayout,
26    selected_theme: String,
27    /// フォントのフォールバック順([0]=プライマリ)。設定アプリで3段セレクトする。
28    selected_fonts: [String; 3],
29    selected_sound: bool,
30    repeat_first_delay: u32,
31    repeat_first_interval: u32,
32    repeat_second_count: u32,
33    repeat_second_delay: u32,
34    repeat_second_interval: u32,
35    selected_caps_ctrl: bool,
36    last_win_w: u32,
37    last_win_h: u32,
38    active_tab_idx: usize,
39    selected_auto_dhcp: bool,
40    focused_item_idx: Option<usize>,
41    // Network タブ
42    ssid_box: TextBox,
43    password_box: TextBox,
44    wifi_focused: u8, // 0=none 1=ssid 2=password
45    wifi_status_msg: String,
46    wifi_connecting: bool,
47    /// 保存・適用の結果表示。失敗を黙って捨てず、必ずユーザーへ見せる。
48    status_msg: String,
49    /// `status_msg` が失敗の通知かどうか(表示色の切り替え用)。
50    status_is_error: bool,
51    // 詳細色編集用
52    custom_theme: config::ThemeConfig,
53    custom_edit_idx: usize,
54    enable_custom_colors: bool,
55    available_themes: alloc::vec::Vec<alloc::string::String>,
56}
57
58impl Default for SettingsApp {
59    fn default() -> Self {
60        Self::new()
61    }
62}
63
64impl SettingsApp {
65    pub fn new() -> Self {
66        let current_config = config::get_config();
67
68        let (fd, fi, sc, sd, si) = unsafe {
69            (
70                crate::REPEAT_PARAMS.first_delay_ms,
71                crate::REPEAT_PARAMS.first_interval_ms,
72                crate::REPEAT_PARAMS.second_repeat_count,
73                crate::REPEAT_PARAMS.second_delay_ms,
74                crate::REPEAT_PARAMS.second_interval_ms,
75            )
76        };
77
78        let wifi_status = if crate::kernel::wifi::is_connected() {
79            let ip = crate::kernel::wifi::get_ip();
80            alloc::format!("Connected — {}.{}.{}.{}", ip[0], ip[1], ip[2], ip[3])
81        } else {
82            String::from("Not connected")
83        };
84
85        let mut available_themes = alloc::vec![
86            String::from("default"),
87            String::from("earth"),
88            String::from("dracula"),
89            String::from("nord"),
90            String::from("sakura"),
91        ];
92
93        // ディスク上の /os/*.json を走査して追加する
94        let fs = crate::kernel::fs::get_fs();
95        let files = fs.list_dir("/os/");
96        for f in files {
97            let name = f.get_filename();
98            if name.ends_with(".json") && name != "/os/config.json" && name != "/os/colorset.json" {
99                if let Some(_pos) = name.rfind('/') {
100                    let tname = name.split('/').next_back().unwrap_or("");
101                    let tname = tname.trim_end_matches(".json").to_string();
102                    if !available_themes.contains(&tname) {
103                        available_themes.push(tname);
104                    }
105                }
106            }
107        }
108
109        Self {
110            selected_layout: current_config.keyboard_layout,
111            selected_theme: current_config.theme_name.clone(),
112            selected_fonts: current_config.font_names.clone(),
113            selected_sound: current_config.enable_sound,
114            repeat_first_delay: fd,
115            repeat_first_interval: fi,
116            repeat_second_count: sc,
117            repeat_second_delay: sd,
118            repeat_second_interval: si,
119            selected_caps_ctrl: current_config.swap_caps_ctrl,
120            last_win_w: 480,
121            last_win_h: 560,
122            active_tab_idx: 0,
123            selected_auto_dhcp: current_config.auto_dhcp,
124            focused_item_idx: None,
125            ssid_box: TextBox::with_text(&current_config.wifi_ssid),
126            password_box: TextBox::with_text(&current_config.wifi_password),
127            wifi_focused: 0,
128            wifi_status_msg: wifi_status,
129            wifi_connecting: false,
130            status_msg: String::new(),
131            status_is_error: false,
132            custom_theme: current_config.custom_theme,
133            custom_edit_idx: 0,
134            enable_custom_colors: current_config.enable_custom_colors,
135            available_themes,
136        }
137    }
138
139    fn get_item_count(&self) -> usize {
140        let tabs = self.tabs();
141        if self.active_tab_idx >= tabs.len() {
142            return 0;
143        }
144        match tabs[self.active_tab_idx].as_str() {
145            "ColorTheme" => {
146                self.available_themes.len() + 1 + 1 // テーマ数 + チェックボックス(1) + Apply(1)
147            }
148            "CustomColor" => {
149                39 + 1 // 13色 * 3チャンネル(39) + Applyボタン(1)
150            }
151            "FontOrder" => 3 + 1,
152            "Keyboard" => 15,
153            "System" => 5,
154            _ => 3,
155        }
156    }
157
158    fn draw_focus_rect(
159        &self,
160        screen: &Screen,
161        x: u32,
162        y: u32,
163        w: u32,
164        h: u32,
165        is_focused: bool,
166        focus_color: Color,
167    ) {
168        if is_focused {
169            screen.boxfill(
170                x.saturating_sub(2),
171                y.saturating_sub(2),
172                x + w + 2,
173                y + h + 2,
174                focus_color,
175            );
176        }
177    }
178
179    fn draw_text_field(
180        &self,
181        screen: &Screen,
182        x: u32,
183        y: u32,
184        w: u32,
185        tb: &TextBox,
186        focused: bool,
187        placeholder: &str,
188        mask: bool,
189    ) {
190        let t = &config::get_config().theme;
191        let border = if focused { t.active_window_border } else { t.browser_secondary };
192        let bg = if focused { t.browser_bg } else { t.ui_input_bg };
193        screen.boxfill(x, y, x + w, y + 28, Color(bg));
194        // border
195        screen.boxfill(x, y, x + w, y + 1, Color(border));
196        screen.boxfill(x, y + 27, x + w, y + 28, Color(border));
197        screen.boxfill(x, y, x + 1, y + 28, Color(border));
198        screen.boxfill(x + w - 1, y, x + w, y + 28, Color(border));
199
200        let display = if tb.text.is_empty() && !focused {
201            String::from(placeholder)
202        } else if mask {
203            tb.text.chars().map(|_| '*').collect()
204        } else {
205            tb.text.clone()
206        };
207
208        let text_color = if tb.text.is_empty() && !focused {
209            t.fn_bar_dim_fg
210        } else {
211            t.login_text_fg
212        };
213        screen.draw_string_vector(x + 8, y + 7, &display, Color(text_color), 13);
214
215        // フォーカス中はカーソル位置に縦棒カーソル(ターミナル共通色 0xFF756CE0)
216        if focused {
217            let mut prefix = String::new();
218            if mask {
219                for _ in 0..tb.cursor_pos {
220                    prefix.push('*');
221                }
222            } else {
223                prefix = tb.text.chars().take(tb.cursor_pos).collect();
224            }
225            let pw = crate::kernel::vector_font::get_vector_string_width(&prefix, 13);
226            let cursor_x = x + 8 + pw;
227            screen.boxfill(
228                cursor_x,
229                y + 6,
230                cursor_x + 1,
231                y + 22,
232                Color(crate::kernel::ime::cursor_color()),
233            );
234        }
235    }
236
237    /// 設定を適用・保存する。
238    ///
239    /// **失敗を握り潰さない**: 以前は保存結果を捨てて常に完了音を鳴らし
240    /// 「Config applied and saved!」と表示していたため、書き込めていなくても
241    /// ユーザーには成功に見えていた。成功したときだけ成功を名乗る。
242    fn apply_and_save(&mut self) {
243        match self.try_apply_and_save() {
244            Ok(()) => {
245                self.status_msg = String::from("Saved.");
246                self.status_is_error = false;
247                crate::kernel::audio::play_system_se(crate::kernel::audio::SeType::Apply);
248                crate::info!("[SYS] Settings: Config applied and saved!");
249            }
250            Err(e) => {
251                self.status_msg = alloc::format!("Save failed: {}", e);
252                self.status_is_error = true;
253                crate::error!("[SYS] Settings: failed to apply/save config: {}", e);
254            }
255        }
256    }
257
258    fn try_apply_and_save(&self) -> Result<(), &'static str> {
259        unsafe {
260            crate::REPEAT_PARAMS.first_delay_ms = self.repeat_first_delay;
261            crate::REPEAT_PARAMS.first_interval_ms = self.repeat_first_interval;
262            crate::REPEAT_PARAMS.second_repeat_count = self.repeat_second_count;
263            crate::REPEAT_PARAMS.second_delay_ms = self.repeat_second_delay;
264            crate::REPEAT_PARAMS.second_interval_ms = self.repeat_second_interval;
265        }
266
267        config::save_config_with_wifi(
268            self.selected_layout,
269            &self.selected_theme,
270            &self.selected_fonts,
271            self.selected_sound,
272            self.selected_caps_ctrl,
273            self.selected_auto_dhcp,
274            &self.ssid_box.text,
275            &self.password_box.text,
276            &self.custom_theme,
277            self.enable_custom_colors,
278        )?;
279
280        if self.enable_custom_colors {
281            let mut obj = alloc::collections::BTreeMap::new();
282            obj.insert(String::from("desktop_bg"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.desktop_bg)));
283            obj.insert(String::from("terminal_bg"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.terminal_bg)));
284            obj.insert(String::from("terminal_fg"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.terminal_fg)));
285            obj.insert(String::from("active_window_border"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.active_window_border)));
286            obj.insert(String::from("non_active_window_border"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.non_active_window_border)));
287            obj.insert(String::from("window_title_bar"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.window_title_bar)));
288            obj.insert(String::from("window_title_fg"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.window_title_fg)));
289            obj.insert(String::from("app_bg"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.app_bg)));
290            obj.insert(String::from("app_fg"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.app_fg)));
291            obj.insert(String::from("button_bg"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.button_bg)));
292            obj.insert(String::from("button_fg"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.button_fg)));
293            obj.insert(String::from("button_active_bg"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.button_active_bg)));
294            obj.insert(String::from("button_active_fg"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.button_active_fg)));
295            obj.insert(String::from("focus_highlight"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.focus_highlight)));
296            obj.insert(String::from("mouse_cursor"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.mouse_cursor)));
297            obj.insert(String::from("mouse_stroke"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.mouse_stroke)));
298            obj.insert(String::from("login_screen_bg"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.login_screen_bg)));
299            obj.insert(String::from("login_dialog_bg"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.login_dialog_bg)));
300            obj.insert(String::from("login_dialog_shadow"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.login_dialog_shadow)));
301            obj.insert(String::from("login_dialog_border"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.login_dialog_border)));
302            obj.insert(String::from("login_text_fg"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.login_text_fg)));
303            obj.insert(String::from("login_field_bg"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.login_field_bg)));
304            obj.insert(String::from("login_field_border"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.login_field_border)));
305            obj.insert(String::from("sysmon_bg"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.sysmon_bg)));
306            obj.insert(String::from("sysmon_header_bg"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.sysmon_header_bg)));
307            obj.insert(String::from("sysmon_accent"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.sysmon_accent)));
308            obj.insert(String::from("sysmon_text"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.sysmon_text)));
309            obj.insert(String::from("sysmon_value"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.sysmon_value)));
310            obj.insert(String::from("sysmon_border"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.sysmon_border)));
311            obj.insert(String::from("tab_area_bg"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.tab_area_bg)));
312            obj.insert(String::from("tab_area_border"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.tab_area_border)));
313            obj.insert(String::from("tab_active_bg"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.tab_active_bg)));
314            obj.insert(String::from("tab_active_fg"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.tab_active_fg)));
315            obj.insert(String::from("tab_inactive_fg"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.tab_inactive_fg)));
316            obj.insert(String::from("fn_bar_bg"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.fn_bar_bg)));
317            obj.insert(String::from("fn_bar_cell_bg"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.fn_bar_cell_bg)));
318            obj.insert(String::from("fn_bar_key_bg"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.fn_bar_key_bg)));
319            obj.insert(String::from("fn_bar_key_fg"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.fn_bar_key_fg)));
320            obj.insert(String::from("fn_bar_text_fg"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.fn_bar_text_fg)));
321            obj.insert(String::from("fn_bar_dim_fg"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.fn_bar_dim_fg)));
322            obj.insert(String::from("fn_bar_border"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.fn_bar_border)));
323            obj.insert(String::from("ime_bg"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.ime_bg)));
324            obj.insert(String::from("ime_border"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.ime_border)));
325            obj.insert(String::from("ime_text"), json::JsonValue::String(alloc::format!("0x{:08X}", self.custom_theme.ime_text)));
326
327            let json_str = json::to_string(&json::JsonValue::Object(obj));
328            let fs = crate::kernel::fs::get_fs();
329            fs.save_file("/os/colorset.json", json_str.as_bytes(), "system,colorset")?;
330        }
331
332        config::reload_and_apply()
333    }
334
335    fn get_color_rgb(c: u32) -> (u8, u8, u8) {
336        (
337            ((c >> 16) & 0xFF) as u8,
338            ((c >> 8) & 0xFF) as u8,
339            (c & 0xFF) as u8,
340        )
341    }
342
343    fn make_color_u32(r: u8, g: u8, b: u8) -> u32 {
344        0xFF000000 | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32)
345    }
346
347    fn get_custom_color_by_idx(&self, idx: usize) -> u32 {
348        match idx {
349            0 => self.custom_theme.desktop_bg,
350            1 => self.custom_theme.terminal_bg,
351            2 => self.custom_theme.terminal_fg,
352            3 => self.custom_theme.active_window_border,
353            4 => self.custom_theme.window_title_bar,
354            5 => self.custom_theme.app_bg,
355            6 => self.custom_theme.app_fg,
356            7 => self.custom_theme.non_active_window_border,
357            8 => self.custom_theme.mouse_cursor,
358            9 => self.custom_theme.login_screen_bg,
359            10 => self.custom_theme.sysmon_bg,
360            11 => self.custom_theme.tab_area_bg,
361            _ => self.custom_theme.fn_bar_bg,
362        }
363    }
364
365    fn set_custom_color_by_idx(&mut self, idx: usize, color: u32) {
366        match idx {
367            0 => self.custom_theme.desktop_bg = color,
368            1 => self.custom_theme.terminal_bg = color,
369            2 => self.custom_theme.terminal_fg = color,
370            3 => self.custom_theme.active_window_border = color,
371            4 => self.custom_theme.window_title_bar = color,
372            5 => self.custom_theme.app_bg = color,
373            6 => self.custom_theme.app_fg = color,
374            7 => self.custom_theme.non_active_window_border = color,
375            8 => self.custom_theme.mouse_cursor = color,
376            9 => self.custom_theme.login_screen_bg = color,
377            10 => self.custom_theme.sysmon_bg = color,
378            11 => self.custom_theme.tab_area_bg = color,
379            _ => self.custom_theme.fn_bar_bg = color,
380        }
381    }
382
383    fn adjust_channel(val: u8, dir: i32) -> u8 {
384        let step = 16i32;
385        let new_val = (val as i32 + dir * step).clamp(0, 255);
386        new_val as u8
387    }
388
389    fn get_custom_color_name(idx: usize) -> &'static str {
390        static CUSTOM_COLOR_NAMES: &[&str] = &[
391            "Desktop BG",
392            "Terminal BG",
393            "Terminal FG",
394            "Active Border",
395            "Title Bar",
396            "App BG",
397            "App FG",
398            "Inactive Border",
399            "Mouse Cursor",
400            "Login Screen BG",
401            "System Monitor BG",
402            "Tab Area BG",
403            "Function Bar BG",
404        ];
405        if idx < CUSTOM_COLOR_NAMES.len() {
406            CUSTOM_COLOR_NAMES[idx]
407        } else {
408            ""
409        }
410    }
411
412    fn get_theme_preview_colors(name: &str) -> (u32, u32, u32) {
413        let fs = crate::kernel::fs::get_fs();
414        let path = alloc::format!("/os/{}.json", name);
415        let mut desktop_bg = 0xFF2EAE3D; // 初期フォールバック
416        let mut terminal_bg = 0xFFF9F5EB;
417        let mut active_border = 0xFFE14D2A;
418
419        if let Some((_, content)) = fs.read_file(&path) {
420            if let Ok(content_str) = core::str::from_utf8(&content) {
421                if let Ok(json::JsonValue::Object(obj)) = json::parse(content_str) {
422                    if let Some(v) = obj.get("desktop_bg") {
423                        if let Some(c) = config::parse_hex_color(v) { desktop_bg = c; }
424                    }
425                    if let Some(v) = obj.get("terminal_bg") {
426                        if let Some(c) = config::parse_hex_color(v) { terminal_bg = c; }
427                    }
428                    if let Some(v) = obj.get("active_window_border") {
429                        if let Some(c) = config::parse_hex_color(v) { active_border = c; }
430                    }
431                }
432            }
433        }
434        (desktop_bg, terminal_bg, active_border)
435    }
436
437    fn start_wifi_connect(&mut self) {
438        let ssid = self.ssid_box.text.clone();
439        let pass = self.password_box.text.clone();
440        if ssid.is_empty() {
441            self.wifi_status_msg = String::from("Error: SSID is empty");
442            return;
443        }
444        self.wifi_status_msg = String::from("Connecting...");
445        self.wifi_connecting = true;
446        let ok = crate::kernel::wifi::connect(&ssid, &pass);
447        if ok {
448            let ip = crate::kernel::wifi::get_ip();
449            self.wifi_status_msg =
450                alloc::format!("Connected — {}.{}.{}.{}", ip[0], ip[1], ip[2], ip[3]);
451            self.apply_and_save();
452        } else {
453            self.wifi_status_msg = String::from("Failed to connect");
454        }
455        self.wifi_connecting = false;
456    }
457
458    fn cycle_font(&mut self, slot: usize, dir: i32) {
459        if slot >= 3 {
460            return;
461        }
462        let list = crate::kernel::vector_font::AVAILABLE_FONTS;
463        let n = list.len() as i32;
464        if n == 0 {
465            return;
466        }
467        let cur = list
468            .iter()
469            .position(|f| *f == self.selected_fonts[slot])
470            .unwrap_or(0) as i32;
471        let next = ((cur + dir) % n + n) % n;
472        self.selected_fonts[slot] = String::from(list[next as usize]);
473    }
474
475    /// 指定テーマのカラーセット `/os/<name>.json` を `/os/colorset.json` へ複製し、
476    /// 設定を再読み込みする。読み出し・書き込み・再読み込みのどの失敗も
477    /// 呼び出し側へ返す(従来は 3 段すべて `let _ =` で捨てていたため、
478    /// テーマを選んでも色が変わらない理由が分からなかった)。
479    fn apply_theme_colorset(&mut self, theme_name: &str) -> Result<(), &'static str> {
480        let fs = crate::kernel::fs::get_fs();
481        let src_path = alloc::format!("/os/{}.json", theme_name);
482        let (_, data) = fs
483            .read_file(&src_path)
484            .ok_or("theme colorset file not found")?;
485        fs.save_file("/os/colorset.json", &data, "system,colorset")?;
486        config::reload_and_apply()
487    }
488
489    /// テーマ選択(キーボード操作・マウス操作の共通処理)。
490    fn select_theme(&mut self, theme_name: &str) {
491        self.selected_theme = String::from(theme_name);
492        self.enable_custom_colors = false;
493
494        match self.apply_theme_colorset(theme_name) {
495            Ok(()) => {
496                self.status_msg = String::new();
497                self.status_is_error = false;
498            }
499            Err(e) => {
500                self.status_msg = alloc::format!("Theme change failed: {}", e);
501                self.status_is_error = true;
502                crate::error!("[SYS] Settings: theme '{}' not applied: {}", theme_name, e);
503            }
504        }
505        self.custom_theme = config::get_config().custom_theme;
506
507        let mut wm = crate::kernel::window_mgr::get_instance();
508        wm.dirty = true;
509    }
510
511    /// カスタムカラーの有効・無効を切り替える(キーボード・マウス共通処理)。
512    fn toggle_custom_colors(&mut self) {
513        self.enable_custom_colors = !self.enable_custom_colors;
514        unsafe {
515            if let Some(ref mut conf) = config::GLOBAL_CONFIG {
516                conf.enable_custom_colors = self.enable_custom_colors;
517                if self.enable_custom_colors {
518                    conf.theme = self.custom_theme;
519                }
520            }
521        }
522        if !self.enable_custom_colors {
523            // プリセットへ戻すので、選択中テーマのカラーセットを再適用する。
524            let name = self.selected_theme.clone();
525            if let Err(e) = self.apply_theme_colorset(&name) {
526                self.status_msg = alloc::format!("Theme restore failed: {}", e);
527                self.status_is_error = true;
528                crate::error!("[SYS] Settings: theme '{}' not restored: {}", name, e);
529            }
530        }
531        let mut wm = crate::kernel::window_mgr::get_instance();
532        wm.dirty = true;
533    }
534
535    fn handle_action(&mut self, idx: usize) {
536        let tabs = self.tabs();
537        if self.active_tab_idx >= tabs.len() {
538            return;
539        }
540        match tabs[self.active_tab_idx].as_str() {
541            "ColorTheme" => {
542                let n_themes = self.available_themes.len();
543                if idx < n_themes {
544                    let theme_name = self.available_themes[idx].clone();
545                    self.select_theme(&theme_name);
546                } else if idx == n_themes {
547                    self.toggle_custom_colors();
548                } else {
549                    self.apply_and_save();
550                }
551            }
552            "CustomColor" => {
553                if idx < 39 {
554                    let color_idx = idx / 3;
555                    let chan_idx = idx % 3;
556                    let color_val = self.get_custom_color_by_idx(color_idx);
557                    let (mut r, mut g, mut b) = Self::get_color_rgb(color_val);
558                    match chan_idx {
559                        0 => r = Self::adjust_channel(r, 1),
560                        1 => g = Self::adjust_channel(g, 1),
561                        _ => b = Self::adjust_channel(b, 1),
562                    }
563                    let new_c = Self::make_color_u32(r, g, b);
564                    self.set_custom_color_by_idx(color_idx, new_c);
565                    if self.enable_custom_colors {
566                        unsafe {
567                            if let Some(ref mut conf) = config::GLOBAL_CONFIG {
568                                conf.custom_theme = self.custom_theme;
569                                conf.theme = self.custom_theme;
570                            }
571                        }
572                        let mut wm = crate::kernel::window_mgr::get_instance();
573                        wm.dirty = true;
574                    }
575                } else {
576                    self.apply_and_save();
577                }
578            }
579            "FontOrder" => {
580                if idx < 3 {
581                    self.cycle_font(idx, 1);
582                } else {
583                    self.apply_and_save();
584                }
585            }
586            "Keyboard" => match idx {
587                0 => self.selected_layout = KeyboardLayout::Jis,
588                1 => self.selected_layout = KeyboardLayout::Us,
589                2 => self.repeat_first_delay = self.repeat_first_delay.saturating_sub(50).max(50),
590                3 => self.repeat_first_delay = self.repeat_first_delay.saturating_add(50).min(2000),
591                4 => {
592                    self.repeat_first_interval = self.repeat_first_interval.saturating_sub(5).max(5)
593                }
594                5 => {
595                    self.repeat_first_interval =
596                        self.repeat_first_interval.saturating_add(5).min(500)
597                }
598                6 => self.repeat_second_count = self.repeat_second_count.saturating_sub(1),
599                7 => self.repeat_second_count = self.repeat_second_count.saturating_add(1).min(100),
600                8 => self.repeat_second_delay = self.repeat_second_delay.saturating_sub(50).max(50),
601                9 => {
602                    self.repeat_second_delay = self.repeat_second_delay.saturating_add(50).min(2000)
603                }
604                10 => {
605                    self.repeat_second_interval =
606                        self.repeat_second_interval.saturating_sub(5).max(5)
607                }
608                11 => {
609                    self.repeat_second_interval =
610                        self.repeat_second_interval.saturating_add(5).min(500)
611                }
612                12 => self.selected_caps_ctrl = true,
613                13 => self.selected_caps_ctrl = false,
614                14 => self.apply_and_save(),
615                _ => {}
616            },
617            "System" => match idx {
618                0 => self.selected_sound = true,
619                1 => self.selected_sound = false,
620                2 => self.selected_auto_dhcp = true,
621                3 => self.selected_auto_dhcp = false,
622                4 => self.apply_and_save(),
623                _ => {}
624            },
625            _ => {}
626        }
627    }
628}
629
630impl App for SettingsApp {
631    fn name(&self) -> &str {
632        "settings"
633    }
634
635    fn tabs(&self) -> alloc::vec::Vec<alloc::string::String> {
636        if self.enable_custom_colors {
637            alloc::vec![
638                String::from("ColorTheme"),
639                String::from("CustomColor"),
640                String::from("FontOrder"),
641                String::from("Keyboard"),
642                String::from("System"),
643                String::from("Network"),
644            ]
645        } else {
646            alloc::vec![
647                String::from("ColorTheme"),
648                String::from("FontOrder"),
649                String::from("Keyboard"),
650                String::from("System"),
651                String::from("Network"),
652            ]
653        }
654    }
655
656    fn active_tab(&self) -> usize {
657        self.active_tab_idx
658    }
659
660    fn switch_tab(&mut self, idx: usize) {
661        let max_tabs = self.tabs().len();
662        self.active_tab_idx = if idx < max_tabs { idx } else { 0 };
663        self.focused_item_idx = None;
664        self.wifi_focused = 0;
665    }
666
667    fn draw(&mut self, screen: &Screen, win_x: u32, win_y: u32, win_w: u32, win_h: u32) {
668        self.last_win_w = win_w;
669        self.last_win_h = win_h;
670
671        let theme = &config::get_config().theme;
672        let bg = Color(theme.app_bg);
673        let fg = Color(theme.app_fg);
674        let btn_bg = Color(theme.button_bg);
675        let btn_fg = Color(theme.button_fg);
676        let btn_act_bg = Color(theme.button_active_bg);
677        let btn_act_fg = Color(theme.button_active_fg);
678        let focus_c = Color(theme.focus_highlight);
679
680        screen.boxfill(win_x, win_y, win_x + win_w, win_y + win_h, bg);
681
682        let tabs = self.tabs();
683        let tab_name = if self.active_tab_idx < tabs.len() {
684            tabs[self.active_tab_idx].as_str()
685        } else {
686            ""
687        };
688
689        let (tab_title, icon) = match tab_name {
690            "ColorTheme" => ("COLOR THEME SETTINGS", ICON_APPEARANCE),
691            "CustomColor" => ("CUSTOM COLOR CONFIG", ICON_APPEARANCE),
692            "FontOrder" => ("FONT FALLBACK ORDER", ICON_APPEARANCE),
693            "Keyboard" => ("KEYBOARD SETTINGS", ICON_KEYBOARD),
694            "System" => ("SYSTEM SETTINGS", ICON_SYSTEM),
695            _ => ("NETWORK SETTINGS", ICON_NETWORK),
696        };
697        screen.draw_svg_icon(win_x + 15, win_y + 12, icon, 24, fg);
698        screen.draw_string_vector(win_x + 45, win_y + 15, tab_title, fg, 18);
699        screen.boxfill(
700            win_x + 15,
701            win_y + 40,
702            win_x + win_w - 15,
703            win_y + 41,
704            Color(theme.fn_bar_border),
705        );
706
707        if tab_name == "ColorTheme" {
708            // === COLOR THEME ===
709            screen.draw_string_vector(win_x + 15, win_y + 55, "Color Theme Presets (JSON):", fg, 15);
710
711            let n_themes = self.available_themes.len();
712            for i in 0..n_themes {
713                let card_x = win_x + 15 + (i as u32 % 2) * 205;
714                let card_y = win_y + 80 + (i as u32 / 2) * 75;
715                let is_focused = self.focused_item_idx == Some(i);
716
717                let theme_name = &self.available_themes[i];
718                let is_selected = !self.enable_custom_colors && self.selected_theme == *theme_name;
719                self.draw_focus_rect(screen, card_x, card_y, 195, 65, is_focused, focus_c);
720                let border_color = if is_selected {
721                    btn_act_bg
722                } else {
723                    Color(theme.fn_bar_border)
724                };
725                screen.boxfill(card_x, card_y, card_x + 195, card_y + 65, border_color);
726                screen.boxfill(
727                    card_x + 2,
728                    card_y + 2,
729                    card_x + 193,
730                    card_y + 63,
731                    Color(theme.login_dialog_bg),
732                );
733
734                screen.draw_string_vector(card_x + 10, card_y + 8, theme_name, Color(theme.app_fg), 13);
735
736                // プレビュー色ボックス描画
737                let (dbg, tbg, aborder) = Self::get_theme_preview_colors(theme_name);
738                screen.boxfill(card_x + 10, card_y + 32, card_x + 45, card_y + 53, Color(dbg));
739                screen.boxfill(card_x + 55, card_y + 32, card_x + 90, card_y + 53, Color(tbg));
740                screen.boxfill(card_x + 100, card_y + 32, card_x + 135, card_y + 53, Color(aborder));
741            }
742
743            // 「Enable Custom Colors」トグルスイッチ
744            let check_y = win_y + 80 + (n_themes as u32).div_ceil(2) * 75 + 10;
745            let check_idx = n_themes;
746            let is_check_focused = self.focused_item_idx == Some(check_idx);
747            self.draw_focus_rect(
748                screen,
749                win_x + 15,
750                check_y,
751                280,
752                28,
753                is_check_focused,
754                focus_c,
755            );
756
757            screen.boxfill(
758                win_x + 15,
759                check_y + 4,
760                win_x + 35,
761                check_y + 24,
762                Color(theme.fn_bar_border),
763            );
764            if self.enable_custom_colors {
765                screen.boxfill(
766                    win_x + 17,
767                    check_y + 6,
768                    win_x + 33,
769                    check_y + 22,
770                    Color(theme.success_fg),
771                );
772            } else {
773                screen.boxfill(
774                    win_x + 17,
775                    check_y + 6,
776                    win_x + 33,
777                    check_y + 22,
778                    Color(theme.login_dialog_bg),
779                );
780            }
781            screen.draw_string_vector(win_x + 45, check_y + 6, "Enable Custom Colors / Edit Colors", fg, 13);
782        } else if tab_name == "CustomColor" {
783            // === CUSTOM COLOR (ALL LIST) ===
784            screen.draw_string_vector(win_x + 15, win_y + 55, "Custom Colors (2 Columns List):", fg, 14);
785
786            for i in 0..13usize {
787                let is_right_col = i >= 7;
788                let col_idx = if is_right_col { i - 7 } else { i };
789                let col_x_offset = if is_right_col { 390u32 } else { 0u32 };
790                let row_y = win_y + 80 + col_idx as u32 * 35;
791
792                let color_name = Self::get_custom_color_name(i);
793                screen.draw_string_vector(win_x + 15 + col_x_offset, row_y + 6, color_name, fg, 11);
794
795                let color_val = self.get_custom_color_by_idx(i);
796                let (r, g, b) = Self::get_color_rgb(color_val);
797                let channels = [r, g, b];
798
799                for chan_idx in 0..3usize {
800                    let cx = win_x + 140 + col_x_offset + (chan_idx as u32) * 72;
801                    let focus_idx = i * 3 + chan_idx;
802                    let is_focused = self.focused_item_idx == Some(focus_idx);
803
804                    // ◀ ボタン
805                    screen.boxfill(cx, row_y, cx + 14, row_y + 24, btn_bg);
806                    screen.draw_string_vector(cx + 4, row_y + 4, "<", btn_fg, 12);
807
808                    // 値ボックス
809                    self.draw_focus_rect(screen, cx + 18, row_y, 32, 24, is_focused, focus_c);
810                    screen.boxfill(cx + 18, row_y, cx + 50, row_y + 24, btn_act_bg);
811                    let val_str = alloc::format!("{:02X}", channels[chan_idx]);
812                    screen.draw_string_vector(cx + 20, row_y + 5, &val_str, btn_act_fg, 10);
813
814                    // ▶ ボタン
815                    screen.boxfill(cx + 54, row_y, cx + 68, row_y + 24, btn_bg);
816                    screen.draw_string_vector(cx + 59, row_y + 4, ">", btn_fg, 12);
817                }
818
819                // プレビュー
820                let px = win_x + 358 + col_x_offset;
821                screen.boxfill(px - 1, row_y - 1, px + 21, row_y + 25, Color(theme.fn_bar_border));
822                screen.boxfill(px, row_y, px + 20, row_y + 24, Color(color_val));
823            }
824        } else if tab_name == "FontOrder" {
825            // === FONT FALLBACK ORDER ===
826            screen.draw_string_vector(win_x + 15, win_y + 55, "Font fallback order:", fg, 16);
827            let labels = ["1st", "2nd", "3rd"];
828            for slot in 0..3usize {
829                let by = win_y + 85 + slot as u32 * 36;
830                let is_focused = self.focused_item_idx == Some(slot);
831                screen.draw_string_vector(win_x + 15, by + 6, labels[slot], fg, 14);
832                screen.boxfill(win_x + 48, by, win_x + 74, by + 28, btn_bg);
833                screen.draw_string_vector(win_x + 56, by + 5, "<", btn_fg, 16);
834                self.draw_focus_rect(screen, win_x + 80, by, 224, 28, is_focused, focus_c);
835                screen.boxfill(win_x + 80, by, win_x + 304, by + 28, btn_act_bg);
836                screen.draw_string_vector(
837                    win_x + 88,
838                    by + 6,
839                    &self.selected_fonts[slot],
840                    btn_act_fg,
841                    14,
842                );
843                screen.boxfill(win_x + 310, by, win_x + 336, by + 28, btn_bg);
844                screen.draw_string_vector(win_x + 318, by + 5, ">", btn_fg, 16);
845            }
846        } else if tab_name == "Keyboard" {
847            // === KEYBOARD ===
848            screen.draw_string_vector(win_x + 15, win_y + 55, "Keyboard Layout:", fg, 16);
849
850            let is_jis = self.selected_layout == KeyboardLayout::Jis;
851            let (jis_bg, jis_fg) = if is_jis {
852                (btn_act_bg, btn_act_fg)
853            } else {
854                (btn_bg, btn_fg)
855            };
856            let (us_bg, us_fg) = if !is_jis {
857                (btn_act_bg, btn_act_fg)
858            } else {
859                (btn_bg, btn_fg)
860            };
861
862            self.draw_focus_rect(
863                screen,
864                win_x + 160,
865                win_y + 50,
866                90,
867                26,
868                self.focused_item_idx == Some(0),
869                focus_c,
870            );
871            screen.boxfill(win_x + 160, win_y + 50, win_x + 250, win_y + 76, jis_bg);
872            screen.draw_string_vector(win_x + 185, win_y + 55, "JIS", jis_fg, 14);
873
874            self.draw_focus_rect(
875                screen,
876                win_x + 265,
877                win_y + 50,
878                90,
879                26,
880                self.focused_item_idx == Some(1),
881                focus_c,
882            );
883            screen.boxfill(win_x + 265, win_y + 50, win_x + 355, win_y + 76, us_bg);
884            screen.draw_string_vector(win_x + 295, win_y + 55, "US", us_fg, 14);
885
886            let repeat_items = [
887                (
888                    "1st Delay:",
889                    self.repeat_first_delay,
890                    "ms",
891                    2usize,
892                    win_y + 90,
893                ),
894                (
895                    "1st Interval:",
896                    self.repeat_first_interval,
897                    "ms",
898                    4,
899                    win_y + 130,
900                ),
901                (
902                    "2nd Count:",
903                    self.repeat_second_count,
904                    "times",
905                    6,
906                    win_y + 170,
907                ),
908                ("2nd Delay:", self.repeat_second_delay, "ms", 8, win_y + 210),
909                (
910                    "2nd Interval:",
911                    self.repeat_second_interval,
912                    "ms",
913                    10,
914                    win_y + 250,
915                ),
916            ];
917            for (label, val, unit, base_idx, by) in repeat_items.iter() {
918                screen.draw_string_vector(win_x + 15, *by + 5, label, fg, 16);
919                let is_minus_focused = self.focused_item_idx == Some(*base_idx);
920                self.draw_focus_rect(screen, win_x + 160, *by, 30, 26, is_minus_focused, focus_c);
921                screen.boxfill(win_x + 160, *by, win_x + 190, *by + 26, btn_bg);
922                screen.draw_string_vector(win_x + 170, *by + 5, "-", btn_fg, 16);
923                let val_str = alloc::format!("{} {}", val, unit);
924                screen.draw_string_vector(win_x + 200, *by + 5, &val_str, fg, 16);
925                let is_plus_focused = self.focused_item_idx == Some(*base_idx + 1);
926                self.draw_focus_rect(screen, win_x + 280, *by, 30, 26, is_plus_focused, focus_c);
927                screen.boxfill(win_x + 280, *by, win_x + 310, *by + 26, btn_bg);
928                screen.draw_string_vector(win_x + 290, *by + 5, "+", btn_fg, 16);
929            }
930
931            screen.draw_string_vector(win_x + 15, win_y + 295, "CapsLock as Ctrl:", fg, 16);
932            let (on_bg, on_fg) = if self.selected_caps_ctrl {
933                (btn_act_bg, btn_act_fg)
934            } else {
935                (btn_bg, btn_fg)
936            };
937            let (off_bg, off_fg) = if !self.selected_caps_ctrl {
938                (Color(theme.error_fg), btn_act_fg)
939            } else {
940                (btn_bg, btn_fg)
941            };
942            self.draw_focus_rect(
943                screen,
944                win_x + 160,
945                win_y + 290,
946                70,
947                26,
948                self.focused_item_idx == Some(12),
949                focus_c,
950            );
951            screen.boxfill(win_x + 160, win_y + 290, win_x + 230, win_y + 316, on_bg);
952            screen.draw_string_vector(win_x + 185, win_y + 295, "ON", on_fg, 14);
953            self.draw_focus_rect(
954                screen,
955                win_x + 245,
956                win_y + 290,
957                70,
958                26,
959                self.focused_item_idx == Some(13),
960                focus_c,
961            );
962            screen.boxfill(win_x + 245, win_y + 290, win_x + 315, win_y + 316, off_bg);
963            screen.draw_string_vector(win_x + 268, win_y + 295, "OFF", off_fg, 14);
964        } else if tab_name == "System" {
965            // === SYSTEM ===
966            screen.draw_string_vector(win_x + 15, win_y + 55, "System Sound:", fg, 16);
967            let (on_bg, on_fg) = if self.selected_sound {
968                (Color(theme.success_fg), btn_act_fg)
969            } else {
970                (btn_bg, btn_fg)
971            };
972            let (off_bg, off_fg) = if !self.selected_sound {
973                (Color(theme.error_fg), btn_act_fg)
974            } else {
975                (btn_bg, btn_fg)
976            };
977            self.draw_focus_rect(
978                screen,
979                win_x + 160,
980                win_y + 50,
981                70,
982                26,
983                self.focused_item_idx == Some(0),
984                focus_c,
985            );
986            screen.boxfill(win_x + 160, win_y + 50, win_x + 230, win_y + 76, on_bg);
987            screen.draw_string_vector(win_x + 185, win_y + 55, "ON", on_fg, 14);
988            self.draw_focus_rect(
989                screen,
990                win_x + 245,
991                win_y + 50,
992                70,
993                26,
994                self.focused_item_idx == Some(1),
995                focus_c,
996            );
997            screen.boxfill(win_x + 245, win_y + 50, win_x + 315, win_y + 76, off_bg);
998            screen.draw_string_vector(win_x + 268, win_y + 55, "OFF", off_fg, 14);
999
1000            screen.draw_string_vector(win_x + 15, win_y + 95, "Auto DHCP on Boot:", fg, 16);
1001            let (dhcp_on_bg, dhcp_on_fg) = if self.selected_auto_dhcp {
1002                (Color(theme.success_fg), btn_act_fg)
1003            } else {
1004                (btn_bg, btn_fg)
1005            };
1006            let (dhcp_off_bg, dhcp_off_fg) = if !self.selected_auto_dhcp {
1007                (Color(theme.error_fg), btn_act_fg)
1008            } else {
1009                (btn_bg, btn_fg)
1010            };
1011            self.draw_focus_rect(
1012                screen,
1013                win_x + 160,
1014                win_y + 90,
1015                70,
1016                26,
1017                self.focused_item_idx == Some(2),
1018                focus_c,
1019            );
1020            screen.boxfill(
1021                win_x + 160,
1022                win_y + 90,
1023                win_x + 230,
1024                win_y + 116,
1025                dhcp_on_bg,
1026            );
1027            screen.draw_string_vector(win_x + 185, win_y + 95, "ON", dhcp_on_fg, 14);
1028            self.draw_focus_rect(
1029                screen,
1030                win_x + 245,
1031                win_y + 90,
1032                70,
1033                26,
1034                self.focused_item_idx == Some(3),
1035                focus_c,
1036            );
1037            screen.boxfill(
1038                win_x + 245,
1039                win_y + 90,
1040                win_x + 315,
1041                win_y + 116,
1042                dhcp_off_bg,
1043            );
1044            screen.draw_string_vector(win_x + 268, win_y + 95, "OFF", dhcp_off_fg, 14);
1045        } else {
1046            // === NETWORK ===
1047            let fw = win_w.saturating_sub(30);
1048
1049            let connected = crate::kernel::wifi::is_connected();
1050            let status_color = if connected {
1051                Color(theme.success_fg)
1052            } else {
1053                Color(theme.error_fg)
1054            };
1055            let status_dot = if connected {
1056                "● Connected"
1057            } else {
1058                "● Not connected"
1059            };
1060            screen.draw_string_vector(win_x + 15, win_y + 55, status_dot, status_color, 15);
1061
1062            if crate::kernel::wifi::is_connected() {
1063                let ip = crate::kernel::wifi::get_ip();
1064                let ip_str = alloc::format!("IP: {}.{}.{}.{}", ip[0], ip[1], ip[2], ip[3]);
1065                screen.draw_string_vector(win_x + 15, win_y + 78, &ip_str, fg, 14);
1066                let ssid = crate::kernel::wifi::get_ssid();
1067                let ssid_str = alloc::format!("SSID: {}", ssid);
1068                screen.draw_string_vector(win_x + 15, win_y + 98, &ssid_str, fg, 14);
1069            }
1070
1071            if !self.wifi_status_msg.is_empty() {
1072                screen.draw_string_vector(
1073                    win_x + 15,
1074                    win_y + 122,
1075                    &self.wifi_status_msg,
1076                    Color(theme.warning_fg),
1077                    13,
1078                );
1079            }
1080
1081            screen.draw_string_vector(win_x + 15, win_y + 152, "SSID:", fg, 14);
1082            let ssid_focused = self.wifi_focused == 1;
1083            self.draw_text_field(
1084                screen,
1085                win_x + 15,
1086                win_y + 170,
1087                fw,
1088                &self.ssid_box,
1089                ssid_focused,
1090                "Enter Wi-Fi network name",
1091                false,
1092            );
1093
1094            screen.draw_string_vector(win_x + 15, win_y + 212, "Password:", fg, 14);
1095            let pass_focused = self.wifi_focused == 2;
1096            self.draw_text_field(
1097                screen,
1098                win_x + 15,
1099                win_y + 230,
1100                fw,
1101                &self.password_box,
1102                pass_focused,
1103                "Enter password",
1104                true,
1105            );
1106
1107            let btn_x0 = win_x + 15;
1108            let btn_y0 = win_y + 278;
1109            let btn_w = 160u32;
1110            let btn_bg_c = if self.wifi_connecting {
1111                Color(theme.fn_bar_dim_fg)
1112            } else {
1113                Color(theme.success_fg)
1114            };
1115            screen.boxfill(btn_x0, btn_y0, btn_x0 + btn_w, btn_y0 + 34, btn_bg_c);
1116            screen.draw_string_vector(
1117                btn_x0 + 22,
1118                btn_y0 + 9,
1119                "Connect / Save",
1120                btn_act_fg,
1121                14,
1122            );
1123
1124            if connected {
1125                let disc_x = btn_x0 + btn_w + 12;
1126                screen.boxfill(disc_x, btn_y0, disc_x + 130, btn_y0 + 34, Color(theme.error_fg));
1127                screen.draw_string_vector(
1128                    disc_x + 10,
1129                    btn_y0 + 9,
1130                    "Disconnect",
1131                    btn_act_fg,
1132                    14,
1133                );
1134            }
1135
1136            screen.draw_string_vector(
1137                win_x + 15,
1138                win_y + 325,
1139                "Click a field and type to edit.",
1140                Color(theme.fn_bar_dim_fg),
1141                12,
1142            );
1143        }
1144
1145        // 保存・適用の結果(成功/失敗)。失敗を黙って捨てないための表示。
1146        if !self.status_msg.is_empty() {
1147            let color = if self.status_is_error {
1148                Color(theme.error_fg)
1149            } else {
1150                Color(theme.success_fg)
1151            };
1152            screen.draw_string_vector(win_x + 15, win_y + win_h - 66, &self.status_msg, color, 13);
1153        }
1154
1155        // Apply ボタン(Network タブ以外)
1156        if tab_name != "Network" {
1157            let btn_x0 = win_x + (win_w - 200) / 2;
1158            let btn_y0 = win_y + win_h - 45;
1159            let apply_idx = self.get_item_count() - 1;
1160            self.draw_focus_rect(
1161                screen,
1162                btn_x0,
1163                btn_y0,
1164                200,
1165                32,
1166                self.focused_item_idx == Some(apply_idx),
1167                focus_c,
1168            );
1169            screen.boxfill(btn_x0, btn_y0, btn_x0 + 200, btn_y0 + 32, Color(theme.success_fg));
1170            screen.draw_string_vector(
1171                btn_x0 + 42,
1172                btn_y0 + 8,
1173                "Apply & Save",
1174                btn_act_fg,
1175                15,
1176            );
1177        }
1178    }
1179
1180    fn on_mouse(
1181        &mut self,
1182        local_x: i32,
1183        local_y: i32,
1184        btn_left: bool,
1185        _btn_right: bool,
1186        _wheel: i32,
1187    ) {
1188        if !btn_left {
1189            return;
1190        }
1191        let x = local_x;
1192        let y = local_y;
1193        let ww = self.last_win_w as i32;
1194        let wh = self.last_win_h as i32;
1195
1196        let tabs = self.tabs();
1197        if self.active_tab_idx >= tabs.len() {
1198            return;
1199        }
1200        let tab_name = tabs[self.active_tab_idx].as_str();
1201
1202        match tab_name {
1203            "ColorTheme" => {
1204                let n_themes = self.available_themes.len();
1205                for i in 0..n_themes {
1206                    let cx = 15 + (i as i32 % 2) * 205;
1207                    let cy = 80 + (i as i32 / 2) * 75;
1208                    if x >= cx && x < cx + 195 && y >= cy && y < cy + 65 {
1209                        let theme_name = self.available_themes[i].clone();
1210                        self.select_theme(&theme_name);
1211                        return;
1212                    }
1213                }
1214
1215                // Enable Custom Colors チェックボックス
1216                let check_y = 80 + ((n_themes as i32 + 1) / 2) * 75 + 10;
1217                if (15..350).contains(&x) && (check_y..check_y + 28).contains(&y) {
1218                    self.toggle_custom_colors();
1219                    return;
1220                }
1221
1222                let bx = (ww - 200) / 2;
1223                let by = wh - 45;
1224                if x >= bx && x < bx + 200 && y >= by && y < by + 32 {
1225                    self.apply_and_save();
1226                }
1227            }
1228            "CustomColor" => {
1229                for i in 0..13usize {
1230                    let is_right_col = i >= 7;
1231                    let col_idx = if is_right_col { i - 7 } else { i };
1232                    let col_x_offset = if is_right_col { 390 } else { 0 };
1233                    let row_y = 80 + col_idx as i32 * 35;
1234
1235                    if y >= row_y && y < row_y + 26 {
1236                        let color_val = self.get_custom_color_by_idx(i);
1237                        let (mut r, mut g, mut b) = Self::get_color_rgb(color_val);
1238
1239                        for chan_idx in 0..3usize {
1240                            let cx = 140 + col_x_offset + (chan_idx as i32) * 72;
1241                            // ◀ ボタン (幅 14px)
1242                            if x >= cx && x < cx + 14 {
1243                                match chan_idx {
1244                                    0 => r = Self::adjust_channel(r, -1),
1245                                    1 => g = Self::adjust_channel(g, -1),
1246                                    _ => b = Self::adjust_channel(b, -1),
1247                                }
1248                                let new_c = Self::make_color_u32(r, g, b);
1249                                self.set_custom_color_by_idx(i, new_c);
1250                                if self.enable_custom_colors {
1251                                    unsafe {
1252                                        if let Some(ref mut conf) = config::GLOBAL_CONFIG {
1253                                            conf.custom_theme = self.custom_theme;
1254                                            conf.theme = self.custom_theme;
1255                                        }
1256                                    }
1257                                    let mut wm = crate::kernel::window_mgr::get_instance();
1258                                    wm.dirty = true;
1259                                }
1260                                return;
1261                            }
1262                            // ▶ ボタン (幅 14px、オフセット cx + 54)
1263                            if x >= cx + 54 && x < cx + 68 {
1264                                match chan_idx {
1265                                    0 => r = Self::adjust_channel(r, 1),
1266                                    1 => g = Self::adjust_channel(g, 1),
1267                                    _ => b = Self::adjust_channel(b, 1),
1268                                }
1269                                let new_c = Self::make_color_u32(r, g, b);
1270                                self.set_custom_color_by_idx(i, new_c);
1271                                if self.enable_custom_colors {
1272                                    unsafe {
1273                                        if let Some(ref mut conf) = config::GLOBAL_CONFIG {
1274                                            conf.custom_theme = self.custom_theme;
1275                                            conf.theme = self.custom_theme;
1276                                        }
1277                                    }
1278                                    let mut wm = crate::kernel::window_mgr::get_instance();
1279                                    wm.dirty = true;
1280                                }
1281                                return;
1282                            }
1283                        }
1284                    }
1285                }
1286
1287                let bx = (ww - 200) / 2;
1288                let by = wh - 45;
1289                if x >= bx && x < bx + 200 && y >= by && y < by + 32 {
1290                    self.apply_and_save();
1291                }
1292            }
1293            "FontOrder" => {
1294                for slot in 0..3usize {
1295                    let by = 85 + slot as i32 * 36;
1296                    if y >= by && y < by + 28 {
1297                        if (48..74).contains(&x) {
1298                            self.cycle_font(slot, -1);
1299                            return;
1300                        }
1301                        if (80..304).contains(&x) || (310..336).contains(&x) {
1302                            self.cycle_font(slot, 1);
1303                            return;
1304                        }
1305                    }
1306                }
1307                let bx = (ww - 200) / 2;
1308                let by = wh - 45;
1309                if x >= bx && x < bx + 200 && y >= by && y < by + 32 {
1310                    self.apply_and_save();
1311                }
1312            }
1313            "Keyboard" => {
1314                if (160..250).contains(&x) && (50..76).contains(&y) {
1315                    self.selected_layout = KeyboardLayout::Jis;
1316                    return;
1317                }
1318                if (265..355).contains(&x) && (50..76).contains(&y) {
1319                    self.selected_layout = KeyboardLayout::Us;
1320                    return;
1321                }
1322                let rows: [(i32, &mut u32, u32, u32, u32); 5] = unsafe {
1323                    let s = &mut *(self as *mut Self);
1324                    [
1325                        (90, &mut s.repeat_first_delay, 50, 2000, 50),
1326                        (130, &mut s.repeat_first_interval, 5, 500, 5),
1327                        (170, &mut s.repeat_second_count, 0, 100, 1),
1328                        (210, &mut s.repeat_second_delay, 50, 2000, 50),
1329                        (250, &mut s.repeat_second_interval, 5, 500, 5),
1330                    ]
1331                };
1332                for (row_y, val, min, max, step) in rows {
1333                    if y >= row_y && y < row_y + 26 {
1334                        if (160..190).contains(&x) {
1335                            *val = val.saturating_sub(step).max(min);
1336                            return;
1337                        }
1338                        if (280..310).contains(&x) {
1339                            *val = (*val + step).min(max);
1340                            return;
1341                        }
1342                    }
1343                }
1344                if (290..316).contains(&y) {
1345                    if (160..230).contains(&x) {
1346                        self.selected_caps_ctrl = true;
1347                        return;
1348                    }
1349                    if (245..315).contains(&x) {
1350                        self.selected_caps_ctrl = false;
1351                        return;
1352                    }
1353                }
1354                let bx = (ww - 200) / 2;
1355                let by = wh - 45;
1356                if x >= bx && x < bx + 200 && y >= by && y < by + 32 {
1357                    self.apply_and_save();
1358                }
1359            }
1360            "System" => {
1361                if (50..76).contains(&y) {
1362                    if (160..230).contains(&x) {
1363                        self.selected_sound = true;
1364                        return;
1365                    }
1366                    if (245..315).contains(&x) {
1367                        self.selected_sound = false;
1368                        return;
1369                    }
1370                }
1371                if (90..116).contains(&y) {
1372                    if (160..230).contains(&x) {
1373                        self.selected_auto_dhcp = true;
1374                        return;
1375                    }
1376                    if (245..315).contains(&x) {
1377                        self.selected_auto_dhcp = false;
1378                        return;
1379                    }
1380                }
1381                let bx = (ww - 200) / 2;
1382                let by = wh - 45;
1383                if x >= bx && x < bx + 200 && y >= by && y < by + 32 {
1384                    self.apply_and_save();
1385                }
1386            }
1387            _ => {
1388                let fw = (ww - 30).max(0);
1389                if x >= 15 && x < 15 + fw && (170..198).contains(&y) {
1390                    self.wifi_focused = 1;
1391                    return;
1392                }
1393                if x >= 15 && x < 15 + fw && (230..258).contains(&y) {
1394                    self.wifi_focused = 2;
1395                    return;
1396                }
1397                if (15..175).contains(&x) && (278..312).contains(&y) {
1398                    self.wifi_focused = 0;
1399                    self.start_wifi_connect();
1400                    return;
1401                }
1402                if crate::kernel::wifi::is_connected()
1403                    && (187..317).contains(&x)
1404                    && (278..312).contains(&y)
1405                {
1406                    crate::kernel::wifi::disconnect();
1407                    self.wifi_status_msg = String::from("Disconnected");
1408                    self.wifi_focused = 0;
1409                    return;
1410                }
1411                self.wifi_focused = 0;
1412            }
1413        }
1414    }
1415
1416    fn on_key(&mut self, keycode: u8, pressed: bool, ascii: Option<char>) {
1417        if !pressed {
1418            return;
1419        }
1420
1421        let tabs = self.tabs();
1422        if self.active_tab_idx >= tabs.len() {
1423            return;
1424        }
1425        let tab_name = tabs[self.active_tab_idx].as_str();
1426
1427        if tab_name == "Network" {
1428            match self.wifi_focused {
1429                1 => {
1430                    if keycode == 0x0F {
1431                        self.wifi_focused = 2;
1432                    } else if keycode == 0x1C {
1433                        self.start_wifi_connect();
1434                    } else {
1435                        let _ = self.ssid_box.handle_key(keycode, ascii);
1436                    }
1437                }
1438                2 => {
1439                    if keycode == 0x0F {
1440                        self.wifi_focused = 1;
1441                    } else if keycode == 0x1C {
1442                        self.start_wifi_connect();
1443                    } else {
1444                        let _ = self.password_box.handle_key(keycode, ascii);
1445                    }
1446                }
1447                _ => {
1448                    if keycode == 0x0F {
1449                        self.wifi_focused = 1;
1450                    }
1451                }
1452            }
1453            return;
1454        }
1455
1456        let count = self.get_item_count();
1457        if ascii == Some('\t') {
1458            if let Some(idx) = self.focused_item_idx {
1459                self.focused_item_idx = Some((idx + 1) % count);
1460            } else {
1461                self.focused_item_idx = Some(0);
1462            }
1463            crate::kernel::audio::play_system_se(crate::kernel::audio::SeType::Apply);
1464        } else if ascii == Some('\n') {
1465            if let Some(idx) = self.focused_item_idx {
1466                self.handle_action(idx);
1467                crate::kernel::audio::play_system_se(crate::kernel::audio::SeType::Apply);
1468            }
1469        }
1470    }
1471}