From e5ce246760e812cc749e95c3ee762a52191a9726 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?I=C3=B1aki=20Dom=C3=ADnguez=20Ochoa?= Date: Sun, 27 Apr 2025 22:57:44 +0200 Subject: [PATCH] very basic scroll --- AmazonQ.md | 41 +++++++++++++ src/app/rendering/mod.rs | 14 +++-- src/app/rendering/terminal_text/mod.rs | 81 +++++++++++++++++++------- 3 files changed, 111 insertions(+), 25 deletions(-) create mode 100644 AmazonQ.md diff --git a/AmazonQ.md b/AmazonQ.md new file mode 100644 index 0000000..10fdb1b --- /dev/null +++ b/AmazonQ.md @@ -0,0 +1,41 @@ +# Text Scrolling Implementation in Simplicitty + +This document outlines the implementation of text scrolling in the Simplicitty terminal emulator project. + +## Overview + +The implementation adds the ability to scroll through text content when it doesn't fit completely in the window. The scrolling is implemented on a line-by-line basis, allowing users to navigate through the content using keyboard shortcuts. + +## Key Components + +1. **Scroll State Management**: + - Added `scroll_offset` to track the current scroll position (in visual lines) + - Added `max_scroll_offset` to limit scrolling to available content + +2. **Viewport Calculation**: + - Modified `get_visible_line_range()` to consider the scroll offset when determining visible lines + - Added `update_max_scroll_offset()` to recalculate the maximum scroll position based on content and viewport size + +3. **Scrolling Methods**: + - `scroll_up(lines)`: Move viewport up by specified number of lines + - `scroll_down(lines)`: Move viewport down by specified number of lines + - `page_up()`: Scroll up by one page (viewport height) + - `page_down()`: Scroll down by one page (viewport height) + +4. **Keyboard Controls**: + - Ctrl+Up/Down: Scroll one line up/down + - Page Up/Down: Scroll one page up/down + - Ctrl+Home/End: Scroll to top/bottom of content + +## Implementation Details + +The scrolling implementation works by adjusting the `scroll_offset` value, which determines which visual lines are rendered in the viewport. The `get_visible_line_range()` function uses this offset to calculate which logical lines should be rendered. + +When the window is resized or content changes, the `max_scroll_offset` is recalculated to ensure scrolling remains within valid bounds. + +## Future Improvements + +1. Add mouse wheel support for scrolling +2. Implement smooth scrolling animations +3. Add a visual scrollbar indicator +4. Preserve horizontal scroll position when navigating vertically diff --git a/src/app/rendering/mod.rs b/src/app/rendering/mod.rs index 6cf8e99..39a83fe 100644 --- a/src/app/rendering/mod.rs +++ b/src/app/rendering/mod.rs @@ -33,6 +33,7 @@ pub struct State { impl State { pub fn resize(&mut self, new_size: PhysicalSize) { + trace!("Resizing window"); if new_size.width > 0 && new_size.height > 0 { self.size = new_size; self.config.width = new_size.width; @@ -44,6 +45,7 @@ impl State { } pub fn render(&mut self) -> Result<(), SurfaceError> { + trace!("Rendering requested"); // Prepare with current configuration (which includes the current window size) self.text.prepare(&self.device, &self.queue, &self.config); @@ -73,6 +75,7 @@ impl State { occlusion_query_set: None, }); + trace!("Rendering..."); self.text.render(&mut _render_pass); } @@ -219,6 +222,7 @@ impl State { Key::Named(k) => { match k { NamedKey::Enter => { + debug!("Receiver enter"); self.text.handle_enter(); } NamedKey::Backspace => { @@ -226,11 +230,8 @@ impl State { if self.dead_key_state.is_some() { self.dead_key_state = None; debug!("Dead key state cleared"); - } else { - // Implement backspace functionality - if self.text.backspace() { - debug!("Backspace processed"); - } + } else if self.text.backspace() { + debug!("Backspace processed"); } } NamedKey::Tab => { @@ -275,18 +276,21 @@ impl State { // Combine dead key with the current character if possible if let Some(combined) = self.combine_with_dead_key(dead_char, s) { for c in combined.chars() { + debug!("Inserting character {c}"); self.text.insert_char(c); } } else { // If combination not possible, insert both separately self.text.insert_char(dead_char); for c in s.chars() { + debug!("Inserting character {c}"); self.text.insert_char(c); } } } else { // Normal character input for c in s.chars() { + debug!("Inserting character {c}"); self.text.insert_char(c); } } diff --git a/src/app/rendering/terminal_text/mod.rs b/src/app/rendering/terminal_text/mod.rs index 49104f1..4e7b0c4 100644 --- a/src/app/rendering/terminal_text/mod.rs +++ b/src/app/rendering/terminal_text/mod.rs @@ -1,6 +1,6 @@ use glyphon::{ - Buffer, BufferLine, Cache, Color, Cursor, FontSystem, LayoutCursor, Metrics, Resolution, - Shaping, SwashCache, TextArea, TextAtlas, TextBounds, TextRenderer, Viewport, Wrap, + Buffer, Cache, Color, Cursor, FontSystem, Metrics, Resolution, SwashCache, TextArea, TextAtlas, + TextBounds, TextRenderer, Viewport, Wrap, }; pub use glyphon::cosmic_text::Motion; @@ -18,6 +18,18 @@ pub struct TerminalText { cursor: Cursor, } +// TODO +// - need to update scroll when +// - inserting into a line and it causes line wrap, creating a new visual line (currently the +// scroll is only updated when creating a logical line or removing the last logical line) +// +// - need to keep track of the scroll vertical offset in two ways: +// - first logical line displayed +// - extra offset that covers possible wrapped lines before the first logical line +// +// - need to remove lines from the top of the buffer when the first logical line displayed is +// bigger than max_scroll_offset + mod safe_casts; use log::{debug, error, trace}; @@ -118,8 +130,9 @@ impl TerminalText { let viewport_height = safe_casts::u32_to_f32_or_max(self.viewport.resolution().height); let line_height = self.buffer.metrics().line_height; - // Start from line 0 (no scrolling yet) - let start_line = 0; + let start_line = self.buffer.scroll().line; + + trace!("Current start line: {start_line}"); // Calculate visible lines based on wrapped text let layout_iter = self.buffer.layout_runs(); @@ -130,8 +143,7 @@ impl TerminalText { last_logical_line = run.line_i; // If we've exceeded the viewport height, we can stop counting - if (safe_casts::usize_to_f32_or_max(start_line + visual_line_count) * line_height) - > viewport_height + if (safe_casts::usize_to_f32_or_max(visual_line_count) * line_height) > viewport_height { break; } @@ -142,6 +154,8 @@ impl TerminalText { 0 } else { // Make sure we include at least the last logical line that has content in the viewport + trace!("Last logical line: {last_logical_line}"); + trace!("Number of lines: {}", self.buffer.lines.len()); (last_logical_line + 1).min(self.buffer.lines.len()) }; @@ -155,27 +169,27 @@ impl TerminalText { for i in start_line..end_line { self.dirty_regions.push(self.get_line_bounds(i)); } - - // Also mark the cursor line as dirty to ensure it's rendered - if self.cursor.line < self.buffer.lines.len() { - self.dirty_regions - .push(self.get_line_bounds(self.cursor.line)); - } } fn merge_dirty_regions(&self) -> TextBounds { if self.dirty_regions.is_empty() { - return TextBounds::default(); // Assuming TextBounds implements Default + return TextBounds::default(); } - self.dirty_regions - .iter() - .fold(TextBounds::default(), |a, b| TextBounds { + self.dirty_regions.iter().fold( + TextBounds { + left: 0, + top: 0, + right: 0, + bottom: 0, + }, + |a, b| TextBounds { left: a.left.min(b.left), top: a.top.min(b.top), right: a.right.max(b.right), bottom: a.bottom.max(b.bottom), - }) + }, + ) } pub fn prepare(&mut self, device: &Device, queue: &Queue, config: &SurfaceConfiguration) { @@ -259,6 +273,7 @@ impl TerminalText { let mut text = this.buffer.lines[line].text().to_string().clone(); let pos = this.cursor.index.min(text.len()); + trace!("Inserting char {c} in line {line}, position {pos}"); text.insert(pos, c); let ending = this.buffer.lines[line].ending(); @@ -286,14 +301,34 @@ impl TerminalText { } let pos = this.cursor.index.min(this.buffer.lines[line].text().len()); + trace!("Inserting newline in line {line}, position {pos}"); let new_line = this.buffer.lines[line].split_off(pos); - // Create a new line with text after cursor - this.buffer.lines.insert(line + 1, new_line); - // Update cursor position to beginning of next line this.cursor.line = line + 1; this.cursor.index = 0; + // Create a new line with text after cursor + this.buffer.lines.insert(this.cursor.line, new_line); + + // Only adjust scroll if cursor would be outside visible area + let mut scroll = this.buffer.scroll(); + let current_scroll_line = + (scroll.vertical / this.buffer.metrics().line_height).floor() as usize; + let max_visible_lines = + (safe_casts::u32_to_f32_or_max(this.viewport.resolution().height) + / this.buffer.metrics().line_height) + .floor() as usize; + + // Only scroll if cursor would be below visible area + if this.cursor.line >= current_scroll_line + max_visible_lines { + scroll.vertical = + safe_casts::usize_to_f32_or_max(this.cursor.line - max_visible_lines + 1) + * this.buffer.metrics().line_height; + trace!("adjusting scroll to keep cursor visible: {:?}", scroll); + this.buffer.set_scroll(scroll); + } else { + trace!("keeping current scroll: {:?}", scroll); + } }) } @@ -342,6 +377,12 @@ impl TerminalText { ret = true; } + + if line > this.buffer.lines.len() { + let mut scroll = this.buffer.scroll(); + scroll.vertical -= 1.0 * this.buffer.metrics().line_height; + this.buffer.set_scroll(scroll); + } ret }) }