very basic scroll
This commit is contained in:
41
AmazonQ.md
Normal file
41
AmazonQ.md
Normal 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
|
||||||
@@ -33,6 +33,7 @@ pub struct State {
|
|||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
pub fn resize(&mut self, new_size: PhysicalSize<u32>) {
|
pub fn resize(&mut self, new_size: PhysicalSize<u32>) {
|
||||||
|
trace!("Resizing window");
|
||||||
if new_size.width > 0 && new_size.height > 0 {
|
if new_size.width > 0 && new_size.height > 0 {
|
||||||
self.size = new_size;
|
self.size = new_size;
|
||||||
self.config.width = new_size.width;
|
self.config.width = new_size.width;
|
||||||
@@ -44,6 +45,7 @@ impl State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(&mut self) -> Result<(), SurfaceError> {
|
pub fn render(&mut self) -> Result<(), SurfaceError> {
|
||||||
|
trace!("Rendering requested");
|
||||||
// Prepare with current configuration (which includes the current window size)
|
// Prepare with current configuration (which includes the current window size)
|
||||||
self.text.prepare(&self.device, &self.queue, &self.config);
|
self.text.prepare(&self.device, &self.queue, &self.config);
|
||||||
|
|
||||||
@@ -73,6 +75,7 @@ impl State {
|
|||||||
occlusion_query_set: None,
|
occlusion_query_set: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
trace!("Rendering...");
|
||||||
self.text.render(&mut _render_pass);
|
self.text.render(&mut _render_pass);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,6 +222,7 @@ impl State {
|
|||||||
Key::Named(k) => {
|
Key::Named(k) => {
|
||||||
match k {
|
match k {
|
||||||
NamedKey::Enter => {
|
NamedKey::Enter => {
|
||||||
|
debug!("Receiver enter");
|
||||||
self.text.handle_enter();
|
self.text.handle_enter();
|
||||||
}
|
}
|
||||||
NamedKey::Backspace => {
|
NamedKey::Backspace => {
|
||||||
@@ -226,11 +230,8 @@ impl State {
|
|||||||
if self.dead_key_state.is_some() {
|
if self.dead_key_state.is_some() {
|
||||||
self.dead_key_state = None;
|
self.dead_key_state = None;
|
||||||
debug!("Dead key state cleared");
|
debug!("Dead key state cleared");
|
||||||
} else {
|
} else if self.text.backspace() {
|
||||||
// Implement backspace functionality
|
debug!("Backspace processed");
|
||||||
if self.text.backspace() {
|
|
||||||
debug!("Backspace processed");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NamedKey::Tab => {
|
NamedKey::Tab => {
|
||||||
@@ -275,18 +276,21 @@ impl State {
|
|||||||
// Combine dead key with the current character if possible
|
// Combine dead key with the current character if possible
|
||||||
if let Some(combined) = self.combine_with_dead_key(dead_char, s) {
|
if let Some(combined) = self.combine_with_dead_key(dead_char, s) {
|
||||||
for c in combined.chars() {
|
for c in combined.chars() {
|
||||||
|
debug!("Inserting character {c}");
|
||||||
self.text.insert_char(c);
|
self.text.insert_char(c);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If combination not possible, insert both separately
|
// If combination not possible, insert both separately
|
||||||
self.text.insert_char(dead_char);
|
self.text.insert_char(dead_char);
|
||||||
for c in s.chars() {
|
for c in s.chars() {
|
||||||
|
debug!("Inserting character {c}");
|
||||||
self.text.insert_char(c);
|
self.text.insert_char(c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Normal character input
|
// Normal character input
|
||||||
for c in s.chars() {
|
for c in s.chars() {
|
||||||
|
debug!("Inserting character {c}");
|
||||||
self.text.insert_char(c);
|
self.text.insert_char(c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use glyphon::{
|
use glyphon::{
|
||||||
Buffer, BufferLine, Cache, Color, Cursor, FontSystem, LayoutCursor, Metrics, Resolution,
|
Buffer, Cache, Color, Cursor, FontSystem, Metrics, Resolution, SwashCache, TextArea, TextAtlas,
|
||||||
Shaping, SwashCache, TextArea, TextAtlas, TextBounds, TextRenderer, Viewport, Wrap,
|
TextBounds, TextRenderer, Viewport, Wrap,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use glyphon::cosmic_text::Motion;
|
pub use glyphon::cosmic_text::Motion;
|
||||||
@@ -18,6 +18,18 @@ pub struct TerminalText {
|
|||||||
cursor: Cursor,
|
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;
|
mod safe_casts;
|
||||||
|
|
||||||
use log::{debug, error, trace};
|
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 viewport_height = safe_casts::u32_to_f32_or_max(self.viewport.resolution().height);
|
||||||
let line_height = self.buffer.metrics().line_height;
|
let line_height = self.buffer.metrics().line_height;
|
||||||
|
|
||||||
// Start from line 0 (no scrolling yet)
|
let start_line = self.buffer.scroll().line;
|
||||||
let start_line = 0;
|
|
||||||
|
trace!("Current start line: {start_line}");
|
||||||
|
|
||||||
// Calculate visible lines based on wrapped text
|
// Calculate visible lines based on wrapped text
|
||||||
let layout_iter = self.buffer.layout_runs();
|
let layout_iter = self.buffer.layout_runs();
|
||||||
@@ -130,8 +143,7 @@ impl TerminalText {
|
|||||||
last_logical_line = run.line_i;
|
last_logical_line = run.line_i;
|
||||||
|
|
||||||
// If we've exceeded the viewport height, we can stop counting
|
// 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)
|
if (safe_casts::usize_to_f32_or_max(visual_line_count) * line_height) > viewport_height
|
||||||
> viewport_height
|
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -142,6 +154,8 @@ impl TerminalText {
|
|||||||
0
|
0
|
||||||
} else {
|
} else {
|
||||||
// Make sure we include at least the last logical line that has content in the viewport
|
// 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())
|
(last_logical_line + 1).min(self.buffer.lines.len())
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -155,27 +169,27 @@ impl TerminalText {
|
|||||||
for i in start_line..end_line {
|
for i in start_line..end_line {
|
||||||
self.dirty_regions.push(self.get_line_bounds(i));
|
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 {
|
fn merge_dirty_regions(&self) -> TextBounds {
|
||||||
if self.dirty_regions.is_empty() {
|
if self.dirty_regions.is_empty() {
|
||||||
return TextBounds::default(); // Assuming TextBounds implements Default
|
return TextBounds::default();
|
||||||
}
|
}
|
||||||
|
|
||||||
self.dirty_regions
|
self.dirty_regions.iter().fold(
|
||||||
.iter()
|
TextBounds {
|
||||||
.fold(TextBounds::default(), |a, b| TextBounds {
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
},
|
||||||
|
|a, b| TextBounds {
|
||||||
left: a.left.min(b.left),
|
left: a.left.min(b.left),
|
||||||
top: a.top.min(b.top),
|
top: a.top.min(b.top),
|
||||||
right: a.right.max(b.right),
|
right: a.right.max(b.right),
|
||||||
bottom: a.bottom.max(b.bottom),
|
bottom: a.bottom.max(b.bottom),
|
||||||
})
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn prepare(&mut self, device: &Device, queue: &Queue, config: &SurfaceConfiguration) {
|
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 mut text = this.buffer.lines[line].text().to_string().clone();
|
||||||
let pos = this.cursor.index.min(text.len());
|
let pos = this.cursor.index.min(text.len());
|
||||||
|
trace!("Inserting char {c} in line {line}, position {pos}");
|
||||||
text.insert(pos, c);
|
text.insert(pos, c);
|
||||||
|
|
||||||
let ending = this.buffer.lines[line].ending();
|
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());
|
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);
|
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
|
// Update cursor position to beginning of next line
|
||||||
this.cursor.line = line + 1;
|
this.cursor.line = line + 1;
|
||||||
this.cursor.index = 0;
|
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;
|
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
|
ret
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user