old school music tracker
1use ascii::{AsciiChar, AsciiString};
2use font8x8::UnicodeFonts;
3use winit::keyboard::{Key, NamedKey};
4
5use crate::{
6 EventQueue,
7 coordinates::{CharPosition, WINDOW_SIZE_CHARS},
8 draw_buffer::DrawBuffer,
9};
10
11use super::{NextWidget, Widget, WidgetResponse};
12
13/// text has max_len of the rect that was given, because the text_in cannot scroll
14/// use text_in_scroll for that
15/// i only allow Ascii characters as i can only render ascii
16pub struct TextIn {
17 pos: CharPosition,
18 width: u8,
19 text: AsciiString,
20 next_widget: NextWidget,
21 // fixed width, so text length is also fixed
22 cursor_pos: u8,
23 #[cfg(feature = "accesskit")]
24 access: (accesskit::NodeId, &'static str),
25}
26
27impl Widget for TextIn {
28 fn draw(&self, draw_buffer: &mut DrawBuffer, selected: bool) {
29 draw_buffer.draw_string_length(self.text.as_str(), self.pos, self.width, 2, 0);
30 // draw the cursor by overdrawing a letter
31 if selected {
32 let cursor_char_pos = self.pos + CharPosition::new(self.cursor_pos, 0);
33 let upos = usize::from(self.cursor_pos);
34 if upos < self.text.len() {
35 draw_buffer.draw_char(
36 font8x8::BASIC_FONTS.get(self.text[upos].into()).unwrap(),
37 cursor_char_pos,
38 0,
39 3,
40 );
41 } else {
42 draw_buffer.draw_rect(3, cursor_char_pos.into());
43 }
44 }
45 }
46
47 fn process_input(
48 &mut self,
49 modifiers: &winit::event::Modifiers,
50 key_event: &winit::event::KeyEvent,
51 _events: &mut EventQueue<'_>,
52 ) -> WidgetResponse {
53 let mut pos = usize::from(self.cursor_pos);
54 let res = process_input(
55 &mut self.text,
56 self.width,
57 &self.next_widget,
58 &mut pos,
59 None,
60 modifiers,
61 key_event,
62 );
63 self.cursor_pos = u8::try_from(pos).unwrap();
64 res
65 }
66
67 #[cfg(feature = "accesskit")]
68 fn build_tree(&self, tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>) {
69 use std::iter::repeat_n;
70
71 use accesskit::{Node, NodeId, Role, TextDirection, TextPosition, TextSelection};
72
73 let mut root_node = Node::new(Role::TextInput);
74 root_node.set_label(self.access.1);
75 let mut text_node = Node::new(Role::TextRun);
76 let text_node_id = NodeId(self.access.0.0 + 1);
77 text_node.set_text_direction(TextDirection::LeftToRight);
78 text_node.set_character_lengths(repeat_n(1, self.text.len()).collect::<Box<[u8]>>());
79 // text_node.set_character_widths(repeat_n(1., self.text.len()).collect::<Box<[f32]>>());
80 // text_node.set_character_positions(
81 // (0..self.text.len())
82 // .map(|n| n as f32)
83 // .collect::<Box<[f32]>>(),
84 // );
85 text_node.set_value(self.text.as_str());
86 // text_node.set_word_lengths(
87 // self.text
88 // .as_str()
89 // .split_whitespace()
90 // .map(|w| u8::try_from(w.len()).unwrap())
91 // .collect::<Box<[u8]>>(),
92 // );
93 let character_index = usize::from(self.cursor_pos);
94 root_node.set_text_selection(TextSelection {
95 anchor: TextPosition {
96 node: text_node_id,
97 character_index,
98 },
99 focus: TextPosition {
100 node: text_node_id,
101 character_index,
102 // character_index: self.text.as_str().len().min(self.cursor_pos + 1),
103 },
104 });
105 root_node.push_child(text_node_id);
106
107 tree.push((self.access.0, root_node));
108 tree.push((text_node_id, text_node));
109 }
110}
111
112impl TextIn {
113 pub fn new(
114 pos: CharPosition,
115 width: u8,
116 next_widget: NextWidget,
117 #[cfg(feature = "accesskit")] access: (accesskit::NodeId, &'static str),
118 ) -> Self {
119 assert!(pos.x() + width < WINDOW_SIZE_CHARS.0);
120 // right and left keys are used in the widget itself. doeesnt make sense to put NextWidget there
121 assert!(next_widget.right.is_none());
122 assert!(next_widget.left.is_none());
123
124 TextIn {
125 pos,
126 width,
127 text: AsciiString::with_capacity(usize::from(width)), // allows to never allocate or deallocate in TextIn
128 next_widget,
129 cursor_pos: 0,
130 #[cfg(feature = "accesskit")]
131 access,
132 }
133 }
134
135 // not tested
136 // TODO: can panic if the string is too long
137 pub fn set_string(&mut self, new_str: String) -> Result<(), ascii::FromAsciiError<String>> {
138 self.text = AsciiString::from_ascii(new_str)?;
139 self.text.truncate(usize::from(self.width));
140 self.cursor_pos = u8::try_from(self.text.len()).unwrap();
141
142 Ok(())
143 }
144
145 pub fn get_str(&self) -> &str {
146 self.text.as_str()
147 }
148}
149
150pub struct TextInScroll {
151 pos: CharPosition,
152 width: u8,
153 text: AsciiString,
154 next_widget: NextWidget,
155 cursor_pos: usize,
156 scroll_offset: usize,
157}
158
159impl Widget for TextInScroll {
160 fn draw(&self, draw_buffer: &mut DrawBuffer, selected: bool) {
161 draw_buffer.draw_string_length(
162 &self.text.as_str()[self.scroll_offset..],
163 self.pos,
164 self.width,
165 2,
166 0,
167 );
168
169 if selected {
170 let cursor_char_pos = self.pos
171 + CharPosition::new(
172 // this minus should make this diff be less then self.width
173 u8::try_from(self.cursor_pos - self.scroll_offset).unwrap(),
174 0,
175 );
176 if self.cursor_pos < self.text.len() {
177 draw_buffer.draw_char(
178 font8x8::BASIC_FONTS
179 .get(self.text[self.cursor_pos].into())
180 .unwrap(),
181 cursor_char_pos,
182 0,
183 3,
184 );
185 } else {
186 draw_buffer.draw_rect(3, cursor_char_pos.into());
187 }
188 }
189 }
190
191 fn process_input(
192 &mut self,
193 modifiers: &winit::event::Modifiers,
194 key_event: &winit::event::KeyEvent,
195 _events: &mut EventQueue<'_>,
196 ) -> WidgetResponse {
197 process_input(
198 &mut self.text,
199 self.width,
200 &self.next_widget,
201 &mut self.cursor_pos,
202 Some(&mut self.scroll_offset),
203 modifiers,
204 key_event,
205 )
206 }
207
208 #[cfg(feature = "accesskit")]
209 fn build_tree(&self, tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>) {
210 todo!()
211 }
212}
213
214impl TextInScroll {
215 pub fn new(pos: CharPosition, width: u8, next_widget: NextWidget) -> Self {
216 assert!(next_widget.right.is_none());
217 assert!(next_widget.left.is_none());
218 assert!(pos.x() + width < WINDOW_SIZE_CHARS.0);
219
220 Self {
221 pos,
222 width,
223 text: AsciiString::new(),
224 next_widget,
225 cursor_pos: 0,
226 scroll_offset: 0,
227 }
228 }
229
230 pub fn get_str(&self) -> &str {
231 self.text.as_str()
232 }
233}
234
235pub fn process_input(
236 text: &mut AsciiString,
237 width: u8,
238 next: &NextWidget,
239 cursor_pos: &mut usize,
240 scroll_offset: Option<&mut usize>,
241 modifiers: &winit::event::Modifiers,
242 key_event: &winit::event::KeyEvent,
243) -> WidgetResponse {
244 /// returns true if the cursor moved
245 fn move_cursor_left(
246 cursor_pos: &mut usize,
247 scroll_offset: Option<&mut usize>,
248 ) -> WidgetResponse {
249 if *cursor_pos > 0 {
250 *cursor_pos -= 1;
251 if let Some(scroll) = scroll_offset
252 && *cursor_pos < *scroll
253 {
254 *scroll -= 1;
255 }
256 // redraw, but no state change
257 WidgetResponse::RequestRedraw(false)
258 } else {
259 WidgetResponse::None
260 }
261 }
262
263 /// returns true if the cursor moved
264 fn move_cursor_right(
265 text: &AsciiString,
266 cursor_pos: &mut usize,
267 width: u8,
268 scroll_offset: Option<&mut usize>,
269 ) -> WidgetResponse {
270 if *cursor_pos < text.len() {
271 *cursor_pos += 1;
272 if let Some(scroll) = scroll_offset
273 && *cursor_pos > *scroll + usize::from(width)
274 {
275 *scroll += 1;
276 }
277 // redraw, but no state change
278 WidgetResponse::RequestRedraw(false)
279 } else {
280 WidgetResponse::None
281 }
282 }
283
284 fn insert_char(
285 text: &mut AsciiString,
286 width: u8,
287 cursor_pos: &mut usize,
288 char: AsciiChar,
289 scroll_offset: Option<&mut usize>,
290 ) {
291 if scroll_offset.is_some() {
292 text.insert(*cursor_pos, char);
293 move_cursor_right(text, cursor_pos, width, scroll_offset);
294 } else {
295 if *cursor_pos < usize::from(width) {
296 *cursor_pos += 1;
297 }
298 text.insert(*cursor_pos - 1, char);
299 text.truncate(usize::from(width));
300 }
301 }
302
303 if !key_event.state.is_pressed() {
304 return WidgetResponse::None;
305 }
306
307 if let Key::Character(str) = &key_event.logical_key {
308 let mut char_iter = str.chars();
309 let first_char = char_iter.next().unwrap();
310 assert!(char_iter.next().is_none());
311
312 if let Ok(ascii_char) = AsciiChar::from_ascii(first_char) {
313 insert_char(text, width, cursor_pos, ascii_char, scroll_offset);
314 WidgetResponse::RequestRedraw(true)
315 } else {
316 WidgetResponse::None
317 }
318 } else if key_event.logical_key == Key::Named(NamedKey::Space) {
319 insert_char(text, width, cursor_pos, AsciiChar::Space, scroll_offset);
320 return WidgetResponse::RequestRedraw(true);
321 } else if key_event.logical_key == Key::Named(NamedKey::ArrowLeft)
322 && modifiers.state().is_empty()
323 {
324 move_cursor_left(cursor_pos, scroll_offset)
325 } else if key_event.logical_key == Key::Named(NamedKey::ArrowRight)
326 && modifiers.state().is_empty()
327 {
328 move_cursor_right(text, cursor_pos, width, scroll_offset)
329 // entf key on german keyboard
330 } else if key_event.logical_key == Key::Named(NamedKey::Delete) {
331 // can't delete if cursor is at the front of the text or the text is empty
332 if *cursor_pos < text.len() {
333 _ = text.remove(*cursor_pos);
334 WidgetResponse::RequestRedraw(true)
335 } else {
336 WidgetResponse::None
337 }
338 } else if key_event.logical_key == Key::Named(NamedKey::Backspace) {
339 // super + backspace clears the string
340 // if the text is already empty we don't need to do anything
341 if modifiers.state().super_key() && !text.is_empty() {
342 text.clear();
343 *cursor_pos = 0;
344 if let Some(scroll) = scroll_offset {
345 *scroll = 0;
346 }
347 WidgetResponse::RequestRedraw(true)
348 } else if modifiers.state().is_empty() && !text.is_empty() {
349 if *cursor_pos == 0 {
350 _ = text.remove(0);
351 } else {
352 _ = text.remove(*cursor_pos - 1);
353 move_cursor_left(cursor_pos, scroll_offset);
354 }
355 WidgetResponse::RequestRedraw(true)
356 } else {
357 WidgetResponse::None
358 }
359 } else {
360 // next widget select
361 next.process_key_event(key_event, modifiers)
362 }
363}