very basic scroll

This commit is contained in:
2025-04-27 22:57:44 +02:00
parent a3fa7eb9d2
commit e5ce246760
3 changed files with 111 additions and 25 deletions

41
AmazonQ.md Normal file
View File

@@ -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

View File

@@ -33,6 +33,7 @@ pub struct State {
impl State {
pub fn resize(&mut self, new_size: PhysicalSize<u32>) {
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);
}
}

View File

@@ -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
})
}