Skip to main content

atmos/kernel/
text_box.rs

1// text_box.rs - 共通テキストボックス編集モジュール
2
3use alloc::string::String;
4
5#[derive(Debug, Clone, Default)]
6pub struct TextBox {
7    pub text: String,
8    pub cursor_pos: usize, // 文字ベースのカーソル位置
9    pub multiline: bool,   // true の場合のみ Tab を 4 桁タブストップ空白へ展開する
10}
11
12/// [`TextBox::handle_key_outcome`] の結果。
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum KeyOutcome {
15    /// このキーは TextBox では扱わなかった(呼び出し側の既定動作へ委ねてよい)。
16    Ignored,
17    /// カーソルだけが動いた。テキストは 1 文字も変わっていない。
18    CursorMoved,
19    /// テキストを書き換えた。
20    TextChanged,
21}
22
23impl TextBox {
24    pub fn new() -> Self {
25        Self {
26            text: String::new(),
27            cursor_pos: 0,
28            multiline: false,
29        }
30    }
31
32    pub fn with_text(text: &str) -> Self {
33        let text_str = String::from(text);
34        let len = text_str.chars().count();
35        Self {
36            text: text_str,
37            cursor_pos: len,
38            multiline: false,
39        }
40    }
41
42    /// 複数行モードを有効にした TextBox を生成します(note エディタ等)。
43    pub fn with_text_multiline(text: &str) -> Self {
44        let mut tb = Self::with_text(text);
45        tb.multiline = true;
46        tb
47    }
48
49    pub fn set_text(&mut self, text: &str) {
50        self.text = String::from(text);
51        self.cursor_pos = self.text.chars().count();
52    }
53
54    /// カーソルを挿入した表示用の文字列を作成します
55    pub fn get_text_with_cursor(&self, show_cursor: bool) -> String {
56        if !show_cursor {
57            return self.text.clone();
58        }
59        let mut s = String::new();
60        for (i, c) in self.text.chars().enumerate() {
61            if i == self.cursor_pos {
62                s.push('|');
63            }
64            s.push(c);
65        }
66        if self.cursor_pos >= self.text.chars().count() {
67            s.push('|');
68        }
69        s
70    }
71
72    /// キーボード入力を処理します。テキストまたはカーソルが動いた場合は true を返します。
73    ///
74    /// 「文字が変わったのか、カーソルが動いただけなのか」を区別したい呼び出し側は
75    /// [`TextBox::handle_key_outcome`] を使うこと。
76    pub fn handle_key(&mut self, keycode: u8, ascii: Option<char>) -> bool {
77        self.handle_key_outcome(keycode, ascii) != KeyOutcome::Ignored
78    }
79
80    /// [`TextBox::handle_key`] の詳細版。矢印キー等の**カーソル移動**と、
81    /// 実際にテキストを書き換えた**編集**を区別して返す。
82    ///
83    /// 【2026-09-08 バグ修正】この区別が無かったため、Web エンジン側は
84    /// 矢印キーでも「値が変わった」とみなして DOM の value を書き換え
85    /// `input` イベントを発火していた。DOM が dirty になると
86    /// `relayout_if_dom_dirty` → `parse_and_layout` が走り、そこで
87    /// `active_text_box` が破棄される。次のキーで `with_text_multiline` により
88    /// **カーソル位置が末尾に戻った** TextBox が作り直されるため、
89    /// note アプリで「↑を押しても 1 行しか上がらない」症状になっていた。
90    pub fn handle_key_outcome(&mut self, keycode: u8, ascii: Option<char>) -> KeyOutcome {
91        let chars_count = self.text.chars().count();
92        if self.cursor_pos > chars_count {
93            self.cursor_pos = chars_count;
94        }
95
96        match keycode {
97            0x0E => {
98                // Backspace
99                if self.cursor_pos > 0 && !self.text.is_empty() {
100                    let mut new_text = String::new();
101                    for (i, c) in self.text.chars().enumerate() {
102                        if i != self.cursor_pos - 1 {
103                            new_text.push(c);
104                        }
105                    }
106                    self.text = new_text;
107                    self.cursor_pos -= 1;
108                    return KeyOutcome::TextChanged;
109                }
110            }
111            0x4B => {
112                // Left Arrow
113                if self.cursor_pos > 0 {
114                    self.cursor_pos -= 1;
115                    return KeyOutcome::CursorMoved;
116                }
117            }
118            0x4D => {
119                // Right Arrow
120                if self.cursor_pos < chars_count {
121                    self.cursor_pos += 1;
122                    return KeyOutcome::CursorMoved;
123                }
124            }
125            0x48 => {
126                // Up Arrow
127                let chars: alloc::vec::Vec<char> = self.text.chars().collect();
128                let mut line_ranges = alloc::vec::Vec::new();
129                let mut start = 0;
130                for (idx, &c) in chars.iter().enumerate() {
131                    if c == '\n' {
132                        line_ranges.push((start, idx));
133                        start = idx + 1;
134                    }
135                }
136                line_ranges.push((start, chars.len()));
137
138                let mut current_line_idx = 0;
139                for (idx, &(start, end)) in line_ranges.iter().enumerate() {
140                    if self.cursor_pos >= start && self.cursor_pos <= end {
141                        current_line_idx = idx;
142                        break;
143                    }
144                }
145
146                if current_line_idx > 0 {
147                    let current_col = self.cursor_pos - line_ranges[current_line_idx].0;
148                    let target_line_idx = current_line_idx - 1;
149                    let (t_start, t_end) = line_ranges[target_line_idx];
150                    let t_len = t_end - t_start;
151                    self.cursor_pos = t_start + current_col.min(t_len);
152                    return KeyOutcome::CursorMoved;
153                }
154            }
155            0x50 => {
156                // Down Arrow
157                let chars: alloc::vec::Vec<char> = self.text.chars().collect();
158                let mut line_ranges = alloc::vec::Vec::new();
159                let mut start = 0;
160                for (idx, &c) in chars.iter().enumerate() {
161                    if c == '\n' {
162                        line_ranges.push((start, idx));
163                        start = idx + 1;
164                    }
165                }
166                line_ranges.push((start, chars.len()));
167
168                let mut current_line_idx = 0;
169                for (idx, &(start, end)) in line_ranges.iter().enumerate() {
170                    if self.cursor_pos >= start && self.cursor_pos <= end {
171                        current_line_idx = idx;
172                        break;
173                    }
174                }
175
176                if current_line_idx < line_ranges.len() - 1 {
177                    let current_col = self.cursor_pos - line_ranges[current_line_idx].0;
178                    let target_line_idx = current_line_idx + 1;
179                    let (t_start, t_end) = line_ranges[target_line_idx];
180                    let t_len = t_end - t_start;
181                    self.cursor_pos = t_start + current_col.min(t_len);
182                    return KeyOutcome::CursorMoved;
183                }
184            }
185            0x53 => {
186                // Delete (Ctrl+D): カーソル位置の文字を前方削除
187                if self.cursor_pos < chars_count {
188                    let mut new_text = String::new();
189                    for (i, c) in self.text.chars().enumerate() {
190                        if i != self.cursor_pos {
191                            new_text.push(c);
192                        }
193                    }
194                    self.text = new_text;
195                    return KeyOutcome::TextChanged;
196                }
197            }
198            0x47 => {
199                // Home (Ctrl+A): 現在行の行頭へ
200                let chars: alloc::vec::Vec<char> = self.text.chars().collect();
201                let mut start = self.cursor_pos;
202                while start > 0 && chars[start - 1] != '\n' {
203                    start -= 1;
204                }
205                if start != self.cursor_pos {
206                    self.cursor_pos = start;
207                    return KeyOutcome::CursorMoved;
208                }
209            }
210            0x4F => {
211                // End (Ctrl+E): 現在行の行末へ
212                let chars: alloc::vec::Vec<char> = self.text.chars().collect();
213                let mut end = self.cursor_pos;
214                while end < chars.len() && chars[end] != '\n' {
215                    end += 1;
216                }
217                if end != self.cursor_pos {
218                    self.cursor_pos = end;
219                    return KeyOutcome::CursorMoved;
220                }
221            }
222            0x49 => {
223                // Page Up: 文書の先頭へ移動
224                if self.cursor_pos != 0 {
225                    self.cursor_pos = 0;
226                    return KeyOutcome::CursorMoved;
227                }
228            }
229            0x51 => {
230                // Page Down: 文書の末尾へ移動
231                if self.cursor_pos != chars_count {
232                    self.cursor_pos = chars_count;
233                    return KeyOutcome::CursorMoved;
234                }
235            }
236            crate::kernel::keyboard::KC_KILL_LINE => {
237                // Ctrl+K: カーソル位置から行末まで削除
238                let chars: alloc::vec::Vec<char> = self.text.chars().collect();
239                let mut end = self.cursor_pos;
240                while end < chars.len() && chars[end] != '\n' {
241                    end += 1;
242                }
243                if end > self.cursor_pos {
244                    let mut new_text = String::new();
245                    for (i, c) in chars.iter().enumerate() {
246                        if i < self.cursor_pos || i >= end {
247                            new_text.push(*c);
248                        }
249                    }
250                    self.text = new_text;
251                    return KeyOutcome::TextChanged;
252                }
253            }
254            0x0F if self.multiline => {
255                // Tab: 4桁タブストップに合わせた半角空白へ展開する(複数行エディタのみ)。
256                // 現在行頭からのカラム位置を求め、次の 4 の倍数までを空白で埋める。
257                let chars: alloc::vec::Vec<char> = self.text.chars().collect();
258                let mut col = 0usize;
259                let mut i = self.cursor_pos;
260                while i > 0 && chars[i - 1] != '\n' {
261                    i -= 1;
262                    col += 1;
263                }
264                let spaces = 4 - (col % 4);
265                let mut new_text = String::new();
266                for (idx, ch) in chars.iter().enumerate() {
267                    if idx == self.cursor_pos {
268                        for _ in 0..spaces {
269                            new_text.push(' ');
270                        }
271                    }
272                    new_text.push(*ch);
273                }
274                if self.cursor_pos >= chars.len() {
275                    for _ in 0..spaces {
276                        new_text.push(' ');
277                    }
278                }
279                self.text = new_text;
280                self.cursor_pos += spaces;
281                return KeyOutcome::TextChanged;
282            }
283            _ => {
284                if let Some(c) = ascii {
285                    if c == '\r' || c == '\n' {
286                        let mut new_text = String::new();
287                        let mut inserted = false;
288                        for (i, ch) in self.text.chars().enumerate() {
289                            if i == self.cursor_pos {
290                                new_text.push('\n');
291                                inserted = true;
292                            }
293                            new_text.push(ch);
294                        }
295                        if !inserted {
296                            new_text.push('\n');
297                        }
298                        self.text = new_text;
299                        self.cursor_pos += 1;
300                        return KeyOutcome::TextChanged;
301                    } else {
302                        let mut new_text = String::new();
303                        let mut inserted = false;
304                        for (i, ch) in self.text.chars().enumerate() {
305                            if i == self.cursor_pos {
306                                new_text.push(c);
307                                inserted = true;
308                            }
309                            new_text.push(ch);
310                        }
311                        if !inserted {
312                            new_text.push(c);
313                        }
314                        self.text = new_text;
315                        self.cursor_pos += 1;
316                        return KeyOutcome::TextChanged;
317                    }
318                }
319            }
320        }
321        KeyOutcome::Ignored
322    }
323}