use ascii::{AsciiChar, AsciiString}; use font8x8::UnicodeFonts; use winit::keyboard::{Key, NamedKey}; use crate::{ EventQueue, coordinates::{CharPosition, WINDOW_SIZE_CHARS}, draw_buffer::DrawBuffer, }; use super::{NextWidget, Widget, WidgetResponse}; /// text has max_len of the rect that was given, because the text_in cannot scroll /// use text_in_scroll for that /// i only allow Ascii characters as i can only render ascii pub struct TextIn { pos: CharPosition, width: u8, text: AsciiString, next_widget: NextWidget, // fixed width, so text length is also fixed cursor_pos: u8, #[cfg(feature = "accesskit")] access: (accesskit::NodeId, &'static str), } impl Widget for TextIn { fn draw(&self, draw_buffer: &mut DrawBuffer, selected: bool) { draw_buffer.draw_string_length(self.text.as_str(), self.pos, self.width, 2, 0); // draw the cursor by overdrawing a letter if selected { let cursor_char_pos = self.pos + CharPosition::new(self.cursor_pos, 0); let upos = usize::from(self.cursor_pos); if upos < self.text.len() { draw_buffer.draw_char( font8x8::BASIC_FONTS.get(self.text[upos].into()).unwrap(), cursor_char_pos, 0, 3, ); } else { draw_buffer.draw_rect(3, cursor_char_pos.into()); } } } fn process_input( &mut self, modifiers: &winit::event::Modifiers, key_event: &winit::event::KeyEvent, _events: &mut EventQueue<'_>, ) -> WidgetResponse { let mut pos = usize::from(self.cursor_pos); let res = process_input( &mut self.text, self.width, &self.next_widget, &mut pos, None, modifiers, key_event, ); self.cursor_pos = u8::try_from(pos).unwrap(); res } #[cfg(feature = "accesskit")] fn build_tree(&self, tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>) { use std::iter::repeat_n; use accesskit::{Node, NodeId, Role, TextDirection, TextPosition, TextSelection}; let mut root_node = Node::new(Role::TextInput); root_node.set_label(self.access.1); let mut text_node = Node::new(Role::TextRun); let text_node_id = NodeId(self.access.0.0 + 1); text_node.set_text_direction(TextDirection::LeftToRight); text_node.set_character_lengths(repeat_n(1, self.text.len()).collect::>()); // text_node.set_character_widths(repeat_n(1., self.text.len()).collect::>()); // text_node.set_character_positions( // (0..self.text.len()) // .map(|n| n as f32) // .collect::>(), // ); text_node.set_value(self.text.as_str()); // text_node.set_word_lengths( // self.text // .as_str() // .split_whitespace() // .map(|w| u8::try_from(w.len()).unwrap()) // .collect::>(), // ); let character_index = usize::from(self.cursor_pos); root_node.set_text_selection(TextSelection { anchor: TextPosition { node: text_node_id, character_index, }, focus: TextPosition { node: text_node_id, character_index, // character_index: self.text.as_str().len().min(self.cursor_pos + 1), }, }); root_node.push_child(text_node_id); tree.push((self.access.0, root_node)); tree.push((text_node_id, text_node)); } } impl TextIn { pub fn new( pos: CharPosition, width: u8, next_widget: NextWidget, #[cfg(feature = "accesskit")] access: (accesskit::NodeId, &'static str), ) -> Self { assert!(pos.x() + width < WINDOW_SIZE_CHARS.0); // right and left keys are used in the widget itself. doeesnt make sense to put NextWidget there assert!(next_widget.right.is_none()); assert!(next_widget.left.is_none()); TextIn { pos, width, text: AsciiString::with_capacity(usize::from(width)), // allows to never allocate or deallocate in TextIn next_widget, cursor_pos: 0, #[cfg(feature = "accesskit")] access, } } // not tested // TODO: can panic if the string is too long pub fn set_string(&mut self, new_str: String) -> Result<(), ascii::FromAsciiError> { self.text = AsciiString::from_ascii(new_str)?; self.text.truncate(usize::from(self.width)); self.cursor_pos = u8::try_from(self.text.len()).unwrap(); Ok(()) } pub fn get_str(&self) -> &str { self.text.as_str() } } pub struct TextInScroll { pos: CharPosition, width: u8, text: AsciiString, next_widget: NextWidget, cursor_pos: usize, scroll_offset: usize, } impl Widget for TextInScroll { fn draw(&self, draw_buffer: &mut DrawBuffer, selected: bool) { draw_buffer.draw_string_length( &self.text.as_str()[self.scroll_offset..], self.pos, self.width, 2, 0, ); if selected { let cursor_char_pos = self.pos + CharPosition::new( // this minus should make this diff be less then self.width u8::try_from(self.cursor_pos - self.scroll_offset).unwrap(), 0, ); if self.cursor_pos < self.text.len() { draw_buffer.draw_char( font8x8::BASIC_FONTS .get(self.text[self.cursor_pos].into()) .unwrap(), cursor_char_pos, 0, 3, ); } else { draw_buffer.draw_rect(3, cursor_char_pos.into()); } } } fn process_input( &mut self, modifiers: &winit::event::Modifiers, key_event: &winit::event::KeyEvent, _events: &mut EventQueue<'_>, ) -> WidgetResponse { process_input( &mut self.text, self.width, &self.next_widget, &mut self.cursor_pos, Some(&mut self.scroll_offset), modifiers, key_event, ) } #[cfg(feature = "accesskit")] fn build_tree(&self, tree: &mut Vec<(accesskit::NodeId, accesskit::Node)>) { todo!() } } impl TextInScroll { pub fn new(pos: CharPosition, width: u8, next_widget: NextWidget) -> Self { assert!(next_widget.right.is_none()); assert!(next_widget.left.is_none()); assert!(pos.x() + width < WINDOW_SIZE_CHARS.0); Self { pos, width, text: AsciiString::new(), next_widget, cursor_pos: 0, scroll_offset: 0, } } pub fn get_str(&self) -> &str { self.text.as_str() } } pub fn process_input( text: &mut AsciiString, width: u8, next: &NextWidget, cursor_pos: &mut usize, scroll_offset: Option<&mut usize>, modifiers: &winit::event::Modifiers, key_event: &winit::event::KeyEvent, ) -> WidgetResponse { /// returns true if the cursor moved fn move_cursor_left( cursor_pos: &mut usize, scroll_offset: Option<&mut usize>, ) -> WidgetResponse { if *cursor_pos > 0 { *cursor_pos -= 1; if let Some(scroll) = scroll_offset && *cursor_pos < *scroll { *scroll -= 1; } // redraw, but no state change WidgetResponse::RequestRedraw(false) } else { WidgetResponse::None } } /// returns true if the cursor moved fn move_cursor_right( text: &AsciiString, cursor_pos: &mut usize, width: u8, scroll_offset: Option<&mut usize>, ) -> WidgetResponse { if *cursor_pos < text.len() { *cursor_pos += 1; if let Some(scroll) = scroll_offset && *cursor_pos > *scroll + usize::from(width) { *scroll += 1; } // redraw, but no state change WidgetResponse::RequestRedraw(false) } else { WidgetResponse::None } } fn insert_char( text: &mut AsciiString, width: u8, cursor_pos: &mut usize, char: AsciiChar, scroll_offset: Option<&mut usize>, ) { if scroll_offset.is_some() { text.insert(*cursor_pos, char); move_cursor_right(text, cursor_pos, width, scroll_offset); } else { if *cursor_pos < usize::from(width) { *cursor_pos += 1; } text.insert(*cursor_pos - 1, char); text.truncate(usize::from(width)); } } if !key_event.state.is_pressed() { return WidgetResponse::None; } if let Key::Character(str) = &key_event.logical_key { let mut char_iter = str.chars(); let first_char = char_iter.next().unwrap(); assert!(char_iter.next().is_none()); if let Ok(ascii_char) = AsciiChar::from_ascii(first_char) { insert_char(text, width, cursor_pos, ascii_char, scroll_offset); WidgetResponse::RequestRedraw(true) } else { WidgetResponse::None } } else if key_event.logical_key == Key::Named(NamedKey::Space) { insert_char(text, width, cursor_pos, AsciiChar::Space, scroll_offset); return WidgetResponse::RequestRedraw(true); } else if key_event.logical_key == Key::Named(NamedKey::ArrowLeft) && modifiers.state().is_empty() { move_cursor_left(cursor_pos, scroll_offset) } else if key_event.logical_key == Key::Named(NamedKey::ArrowRight) && modifiers.state().is_empty() { move_cursor_right(text, cursor_pos, width, scroll_offset) // entf key on german keyboard } else if key_event.logical_key == Key::Named(NamedKey::Delete) { // can't delete if cursor is at the front of the text or the text is empty if *cursor_pos < text.len() { _ = text.remove(*cursor_pos); WidgetResponse::RequestRedraw(true) } else { WidgetResponse::None } } else if key_event.logical_key == Key::Named(NamedKey::Backspace) { // super + backspace clears the string // if the text is already empty we don't need to do anything if modifiers.state().super_key() && !text.is_empty() { text.clear(); *cursor_pos = 0; if let Some(scroll) = scroll_offset { *scroll = 0; } WidgetResponse::RequestRedraw(true) } else if modifiers.state().is_empty() && !text.is_empty() { if *cursor_pos == 0 { _ = text.remove(0); } else { _ = text.remove(*cursor_pos - 1); move_cursor_left(cursor_pos, scroll_offset); } WidgetResponse::RequestRedraw(true) } else { WidgetResponse::None } } else { // next widget select next.process_key_event(key_event, modifiers) } }