diff --git a/gui/src/rust/ensogl/src/display/shape/text/text_field.rs b/gui/src/rust/ensogl/src/display/shape/text/text_field.rs index b642477a0f0..57692b214f8 100644 --- a/gui/src/rust/ensogl/src/display/shape/text/text_field.rs +++ b/gui/src/rust/ensogl/src/display/shape/text/text_field.rs @@ -26,13 +26,13 @@ use crate::display::shape::text::glyph::font::FontRegistry; use crate::display::shape::text::text_field::render::TextFieldSprites; use crate::display::shape::text::text_field::render::assignment::GlyphLinesAssignmentUpdate; use crate::display::world::World; +use crate::system::web::text_input::KeyboardBinding; use data::text::TextChange; use data::text::TextLocation; use nalgebra::Vector2; use nalgebra::Vector3; use nalgebra::Vector4; -use crate::system::web::text_input::KeyboardBinding; @@ -148,28 +148,28 @@ shared! { TextField self.properties.size } - /// Scroll one page down. - pub fn page_down(&mut self) { - self.scroll(Vector2::new(0.0, -self.size().y)) - } - - /// Scroll one page up. - pub fn page_up(&mut self) { - self.scroll(Vector2::new(0.0, self.size().y)) - } - /// Scroll text by given offset in pixels. pub fn scroll(&mut self, offset:Vector2) { - let position_change = -Vector3::new(offset.x,offset.y,0.0); - self.rendered.display_object.mod_position(|pos| *pos += position_change); - let mut update = self.assignment_update(); - if offset.x != 0.0 { - update.update_after_x_scroll(offset.x); + let scroll_position = self.scroll_position(); + let padding_lines = 2; + let lines = self.content.lines().len() + padding_lines; + let text_height = self.content.line_height * lines as f32; + let view_height = self.size().y; + let height = (text_height - view_height).max(0.0); + let offset_y = offset.y.min(scroll_position.y).max(scroll_position.y - height); + let offset = Vector2::new(offset.x, offset_y); + if offset.x != 0.0 || offset.y != 0.0 { + let position_change = -Vector3::new(offset.x,offset.y,0.0); + self.rendered.display_object.mod_position(|pos| *pos += position_change); + let mut update = self.assignment_update(); + if offset.x != 0.0 { + update.update_after_x_scroll(offset.x); + } + if offset.y != 0.0 { + update.update_line_assignment(); + } + self.rendered.update_glyphs(&mut self.content); } - if offset.y != 0.0 { - update.update_line_assignment(); - } - self.rendered.update_glyphs(&mut self.content); } /// Get current scroll position. @@ -204,21 +204,51 @@ shared! { TextField /// Jump active cursor to point on the screen. pub fn jump_cursor(&mut self, point:Vector2, selecting:bool) { - let point_on_text = self.relative_position(point); - let content = &mut self.content; - let mut navigation = CursorNavigation {selecting, ..CursorNavigation::default(content)}; + let point_on_text = self.relative_position(point); + let text_field_size = self.size(); + let content = &mut self.content; + let mut navigation = CursorNavigation{selecting,content,text_field_size}; self.cursors.jump_cursor(&mut navigation,point_on_text); self.rendered.update_cursor_sprites(&self.cursors, &mut self.content,self.focused); } + /// Processes PageUp and PageDown, scrolling the page accordingly. + fn scroll_page(&mut self, step:Step) { + let page_height = self.size().y; + let scrolling = match step { + Step::PageUp => page_height, + Step::PageDown => -page_height, + _ => 0.0 + }; + + self.scroll(Vector2::new(0.0,scrolling)); + } + + /// Adjust the view to make the last cursor visible. + fn adjust_view(&mut self) { + let last_cursor = self.cursors.last_cursor(); + let scroll_y = self.scroll_position().y; + let view_size = self.size(); + let current_line = last_cursor.current_line(&mut self.content); + let current_line_pos = current_line.y_position(); + let next_line_pos = current_line_pos + current_line.height; + let y_scrolling = (scroll_y - next_line_pos + view_size.y).min(0.0); + let y_scrolling = (scroll_y - current_line_pos).max(y_scrolling); + let scrolling = Vector2::new(0.0,y_scrolling); + self.scroll(scrolling); + } + /// Move all cursors by given step. pub fn navigate_cursors(&mut self, step:Step, selecting:bool) { if !selecting { self.clear_word_occurrences() } - let content = &mut self.content; - let mut navigation = CursorNavigation {content,selecting}; + let text_field_size = self.size(); + let content = &mut self.content; + let mut navigation = CursorNavigation{content,selecting,text_field_size}; self.cursors.navigate_all_cursors(&mut navigation,step); + self.scroll_page(step); + self.adjust_view(); self.rendered.update_cursor_sprites(&self.cursors, &mut self.content,self.focused); } @@ -370,6 +400,7 @@ impl TextField { // TODO[ao] updates should be done only in one place and only once per frame // see https://github.com/luna/ide/issues/178 this.assignment_update().update_after_text_edit(); + this.adjust_view(); this.rendered.update_glyphs(&mut this.content); this.rendered.update_cursor_sprites(&this.cursors, &mut this.content, this.focused); }); @@ -385,11 +416,11 @@ impl TextField { /// For cursors with selection it will just remove the selected text. For the rest, it will /// remove all content covered by `step`. pub fn do_delete_operation(&self, step:Step) { + let text_field_size = self.size(); self.with_borrowed(|this| { let content = &mut this.content; let selecting = true; - let mut navigation = CursorNavigation - {selecting,..CursorNavigation::default(content)}; + let mut navigation = CursorNavigation{selecting,content,text_field_size}; let without_selection = |c:&Cursor| !c.has_selection(); this.cursors.navigate_cursors(&mut navigation,step,without_selection); }); diff --git a/gui/src/rust/ensogl/src/display/shape/text/text_field/content/line.rs b/gui/src/rust/ensogl/src/display/shape/text/text_field/content/line.rs index 9ecc13b3bd3..14aa9400c30 100644 --- a/gui/src/rust/ensogl/src/display/shape/text/text_field/content/line.rs +++ b/gui/src/rust/ensogl/src/display/shape/text/text_field/content/line.rs @@ -8,7 +8,6 @@ use nalgebra::Vector2; use std::ops::Range; - // ============ // === Line === // ============ @@ -99,11 +98,16 @@ pub struct LineFullInfo<'a> { } impl<'a> LineFullInfo<'a> { + /// Get the y position of the top of the line. + pub fn y_position(&self) -> f32 { + self.height * self.line_id as f32 + } + /// Get the point where a _baseline_ of current line begins (The _baseline_ is a font specific /// term, for details see [freetype documentation] /// (https://www.freetype.org/freetype2/docs/glyphs/glyphs-3.html#section-1)). pub fn baseline_start(&self) -> Vector2 { - Vector2::new(0.0, (-(self.line_id as f32) - 0.85) * self.height) + Vector2::new(0.0, -self.y_position() - self.height * 0.85) } /// Get x position of character with given index. The position is in _text space_. diff --git a/gui/src/rust/ensogl/src/display/shape/text/text_field/cursor.rs b/gui/src/rust/ensogl/src/display/shape/text/text_field/cursor.rs index 0da1c3ceb1f..4ef42d82f5d 100644 --- a/gui/src/rust/ensogl/src/display/shape/text/text_field/cursor.rs +++ b/gui/src/rust/ensogl/src/display/shape/text/text_field/cursor.rs @@ -133,11 +133,14 @@ impl Cursor { /// Home, End, Ctrl+Home, etc.) #[derive(Copy,Clone,Debug,Eq,Hash,PartialEq)] #[allow(missing_docs)] -pub enum Step {Left,LeftWord,Right,RightWord,Up,Down,LineBegin,LineEnd,DocBegin,DocEnd} +pub enum Step +{Left,LeftWord,Right,RightWord,PageUp,Up,PageDown,Down,LineBegin,LineEnd,DocBegin,DocEnd} /// A struct for cursor navigation process. #[derive(Debug)] pub struct CursorNavigation<'a> { + /// A snapshot of TextField's size. + pub text_field_size: Vector2, /// A reference to text content. This is required to obtain the x positions of chars for proper /// moving cursors up and down. pub content: &'a mut TextFieldContent, @@ -146,12 +149,6 @@ pub struct CursorNavigation<'a> { } impl<'a> CursorNavigation<'a> { - /// Creates a new CursorNavigation with defaults. - pub fn default(content:&'a mut TextFieldContent) -> Self { - let selecting = default(); - Self {content,selecting} - } - /// Jump cursor directly to given position. pub fn move_cursor_to_position(&self, cursor:&mut Cursor, to:TextLocation) { cursor.position = to; @@ -220,16 +217,18 @@ impl<'a> CursorNavigation<'a> { /// Get cursor position one line above the given position, such the new x coordinate of /// displayed cursor on the screen will be nearest the current value. - pub fn line_up_position(&mut self, position:&TextLocation) -> Option { - let prev_line = position.line.checked_sub(1); - prev_line.map(|line| self.near_same_x_in_another_line(position,line)) + pub fn line_up_position(&mut self, position:&TextLocation, lines:usize) -> TextLocation { + let prev_line = position.line.checked_sub(lines); + let prev_line = prev_line.map(|line| self.near_same_x_in_another_line(position,line)); + prev_line.unwrap_or_else(TextLocation::at_document_begin) } /// Get cursor position one line behind the given position, such the new x coordinate of /// displayed cursor on the screen will be nearest the current value. - pub fn line_down_position(&mut self, position:&TextLocation) -> Option { - let next_line = Some(position.line + 1).filter(|l| *l < self.content.lines().len()); - next_line.map(|line| self.near_same_x_in_another_line(position,line)) + pub fn line_down_position(&mut self, position:&TextLocation, lines:usize) -> TextLocation { + let next_line = Some(position.line + lines).filter(|l| *l < self.content.lines().len()); + let next_line = next_line.map(|line| self.near_same_x_in_another_line(position,line)); + next_line.unwrap_or_else(|| self.content_end_position()) } /// Returns the next column if it exists. If it doesn't exist it attempts to return the @@ -277,6 +276,10 @@ impl<'a> CursorNavigation<'a> { Self::next_valid_text_location(line, previous_line, previous_column, line_end) } + fn get_lines_from_height(&self) -> usize { + (self.text_field_size.y / self.content.line_height) as usize + } + /// New position of cursor at `position` after applying `step`. fn new_position(&mut self, position: TextLocation, step:Step) -> TextLocation { match step { @@ -284,8 +287,10 @@ impl<'a> CursorNavigation<'a> { Step::RightWord => self.next_word_position(&position).unwrap_or(position), Step::Left => self.prev_char_position(&position).unwrap_or(position), Step::Right => self.next_char_position(&position).unwrap_or(position), - Step::Up => self.line_up_position(&position).unwrap_or(position), - Step::Down => self.line_down_position(&position).unwrap_or(position), + Step::PageUp => self.line_up_position(&position,self.get_lines_from_height()), + Step::PageDown => self.line_down_position(&position,self.get_lines_from_height()), + Step::Up => self.line_up_position(&position,1), + Step::Down => self.line_down_position(&position,1), Step::LineBegin => TextLocation::at_line_begin(position.line), Step::LineEnd => self.line_end_position(position.line), Step::DocBegin => TextLocation::at_document_begin(), @@ -587,13 +592,19 @@ mod test { expected_positions.insert(LineEnd, vec![(0,10),(1,10),(2,9)]); expected_positions.insert(DocBegin, vec![(0,0)]); expected_positions.insert(DocEnd, vec![(2,9)]); + expected_positions.insert(PageUp, vec![(0,0),(0,9)]); + expected_positions.insert(PageDown, vec![(2,0),(2,9)]); - let mut fonts = FontRegistry::new(); - let properties = TextFieldProperties::default(&mut fonts); - let mut content = TextFieldContent::new(text,&properties); - let mut navigation = CursorNavigation::default(&mut content); + let mut fonts = FontRegistry::new(); + let mut properties = TextFieldProperties::default(&mut fonts); + let two_lines_high = properties.text_size * 2.0; + properties.size = Vector2::new(10.0, two_lines_high); + let content = &mut TextFieldContent::new(text,&properties); + let text_field_size = properties.size; + let selecting = false; + let mut navigation = CursorNavigation{selecting,content,text_field_size}; - for step in &[/*Left,Right,Up,*/Down,/*LineBegin,LineEnd,DocBegin,DocEnd*/] { + for step in &[Left,Right,Up,Down,LineBegin,LineEnd,DocBegin,DocEnd,PageUp,PageDown] { let mut cursors = Cursors::mock(initial_cursors.clone()); cursors.navigate_all_cursors(&mut navigation,*step); let expected = expected_positions.get(step).unwrap(); @@ -613,16 +624,39 @@ mod test { let initial_cursors = vec![initial_cursor]; let new_position = TextLocation {line:1,column:10}; - let mut fonts = FontRegistry::new(); - let properties = TextFieldProperties::default(&mut fonts); - let mut content = TextFieldContent::new(text,&properties); - let mut navigation = CursorNavigation::default(&mut content); + let mut fonts = FontRegistry::new(); + let properties = TextFieldProperties::default(&mut fonts); + let content = &mut TextFieldContent::new(text,&properties); + let selecting = false; + let text_field_size = properties.size; + let mut navigation = CursorNavigation{content,text_field_size,selecting}; let mut cursors = Cursors::mock(initial_cursors.clone()); cursors.navigate_all_cursors(&mut navigation,LineEnd); assert_eq!(new_position, cursors.first_cursor().position); assert_eq!(new_position, cursors.first_cursor().selected_to); } + #[wasm_bindgen_test(async)] + async fn page_scrolling() { + ensogl_core_msdf_sys::initialized().await; + let text = "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19"; + let initial_cursor = Cursor::new(TextLocation::at_document_begin()); + let initial_cursors = vec![initial_cursor]; + let expected_position = TextLocation {line:6,column:0}; + + let mut fonts = FontRegistry::new(); + let mut properties = TextFieldProperties::default(&mut fonts); + properties.size = Vector2::new(100.0,100.0); + let content = &mut TextFieldContent::new(text,&properties); + let selecting = false; + let text_field_size = properties.size; + let mut navigation = CursorNavigation{selecting,content,text_field_size}; + let mut cursors = Cursors::mock(initial_cursors.clone()); + cursors.navigate_all_cursors(&mut navigation,PageDown); + assert_eq!(expected_position, cursors.first_cursor().position); + assert_eq!(expected_position, cursors.first_cursor().selected_to); + } + #[wasm_bindgen_test(async)] async fn moving_with_select() { ensogl_core_msdf_sys::initialized().await; @@ -631,11 +665,12 @@ mod test { let initial_cursors = vec![Cursor::new(initial_loc)]; let new_loc = TextLocation {line:0,column:9}; - let mut fonts = FontRegistry::new(); - let properties = TextFieldProperties::default(&mut fonts); - let mut content = TextFieldContent::new(text,&properties); - let mut navigation = CursorNavigation - {selecting:true, ..CursorNavigation::default(&mut content)}; + let mut fonts = FontRegistry::new(); + let properties = TextFieldProperties::default(&mut fonts); + let content = &mut TextFieldContent::new(text,&properties); + let selecting = true; + let text_field_size = properties.size; + let mut navigation = CursorNavigation{selecting,content,text_field_size}; let mut cursors = Cursors::mock(initial_cursors.clone()); cursors.navigate_all_cursors(&mut navigation,LineEnd); assert_eq!(new_loc , cursors.first_cursor().position); @@ -697,12 +732,14 @@ mod test { #[wasm_bindgen_test(async)] async fn step_into_word() { msdf_sys::initialized().await; - let content = "first sentence\r\nthis is a second sentence\r\nlast sentence\n"; - let content = &mut TextFieldContent::new(content,&mock_properties()); - let selecting = false; - let mut navigation = CursorNavigation{content,selecting}; - let mut location = TextLocation::at_document_begin(); - location = navigation.next_word_position(&location).unwrap(); + let content = "first sentence\r\nthis is a second sentence\r\nlast sentence\n"; + let properties = mock_properties(); + let content = &mut TextFieldContent::new(content,&properties); + let selecting = false; + let text_field_size = properties.size; + let mut navigation = CursorNavigation{content,selecting,text_field_size}; + let mut location = TextLocation::at_document_begin(); + location = navigation.next_word_position(&location).unwrap(); assert_eq!(location, TextLocation{line:0, column:5}); location = navigation.next_word_position(&location).unwrap(); assert_eq!(location, TextLocation{line:0, column:14}); diff --git a/gui/src/rust/ensogl/src/display/shape/text/text_field/frp/keyboard.rs b/gui/src/rust/ensogl/src/display/shape/text/text_field/frp/keyboard.rs index 84db91801f5..44c89faef42 100644 --- a/gui/src/rust/ensogl/src/display/shape/text/text_field/frp/keyboard.rs +++ b/gui/src/rust/ensogl/src/display/shape/text/text_field/frp/keyboard.rs @@ -150,6 +150,8 @@ impl TextFieldKeyboardFrp { setter.set_navigation_action(&[ArrowRight], Step::Right); setter.set_navigation_action(&[ArrowUp], Step::Up); setter.set_navigation_action(&[ArrowDown], Step::Down); + setter.set_navigation_action(&[PageDown], Step::PageDown); + setter.set_navigation_action(&[PageUp], Step::PageUp); setter.set_navigation_action(&[Home], Step::LineBegin); setter.set_navigation_action(&[End], Step::LineEnd); setter.set_navigation_action(&[Control,Home], Step::DocBegin); @@ -161,8 +163,6 @@ impl TextFieldKeyboardFrp { setter.set_action(&[Delete], |t| t.do_delete_operation(Step::Right)); setter.set_action(&[Backspace], |t| t.do_delete_operation(Step::Left)); setter.set_action(&[Escape], |t| t.finish_multicursor_mode()); - setter.set_action(&[PageDown], |t| t.page_down()); - setter.set_action(&[PageUp], |t| t.page_up()); } } diff --git a/gui/src/rust/ide/src/view/text_editor.rs b/gui/src/rust/ide/src/view/text_editor.rs index 0b18b268740..76ba29af9d7 100644 --- a/gui/src/rust/ide/src/view/text_editor.rs +++ b/gui/src/rust/ide/src/view/text_editor.rs @@ -77,7 +77,7 @@ impl TextEditor { let font = fonts.get_or_load_embedded_font("DejaVuSansMono").unwrap(); let padding = default(); let position = zero(); - let size = Vector2::new(screen.width, screen.height); + let size = Vector2::new(screen.width, screen.height / 2.0); let black = Vector4::new(0.0,0.0,0.0,1.0); let base_color = black; let text_size = 16.0;