Enhance TUI scrolling: add viewport dimension tracking, autoscroll logic, and message line calculation. Refactor related components for dynamic rendering.
This commit is contained in:
@@ -64,6 +64,8 @@ pub struct ChatApp {
|
|||||||
pub selected_provider_index: usize, // Index into the available_providers list
|
pub selected_provider_index: usize, // Index into the available_providers list
|
||||||
pub selected_model: Option<usize>, // Index into the *filtered* models list
|
pub selected_model: Option<usize>, // Index into the *filtered* models list
|
||||||
scroll: usize,
|
scroll: usize,
|
||||||
|
viewport_height: usize, // Track the height of the messages viewport
|
||||||
|
content_width: usize, // Track the content width for line wrapping calculations
|
||||||
session_tx: mpsc::UnboundedSender<SessionEvent>,
|
session_tx: mpsc::UnboundedSender<SessionEvent>,
|
||||||
streaming: HashSet<Uuid>,
|
streaming: HashSet<Uuid>,
|
||||||
textarea: TextArea<'static>, // Advanced text input widget
|
textarea: TextArea<'static>, // Advanced text input widget
|
||||||
@@ -86,6 +88,8 @@ impl ChatApp {
|
|||||||
selected_provider_index: 0,
|
selected_provider_index: 0,
|
||||||
selected_model: None,
|
selected_model: None,
|
||||||
scroll: 0,
|
scroll: 0,
|
||||||
|
viewport_height: 10, // Default viewport height, will be updated during rendering
|
||||||
|
content_width: 80, // Default content width, will be updated during rendering
|
||||||
session_tx,
|
session_tx,
|
||||||
streaming: std::collections::HashSet::new(),
|
streaming: std::collections::HashSet::new(),
|
||||||
textarea,
|
textarea,
|
||||||
@@ -396,9 +400,9 @@ impl ChatApp {
|
|||||||
message_id,
|
message_id,
|
||||||
response,
|
response,
|
||||||
} => {
|
} => {
|
||||||
let at_bottom = self.scroll == 0;
|
let was_at_bottom = self.is_at_bottom();
|
||||||
self.controller.apply_stream_chunk(message_id, &response)?;
|
self.controller.apply_stream_chunk(message_id, &response)?;
|
||||||
if at_bottom {
|
if was_at_bottom {
|
||||||
self.scroll_to_bottom();
|
self.scroll_to_bottom();
|
||||||
}
|
}
|
||||||
if response.is_final {
|
if response.is_final {
|
||||||
@@ -559,8 +563,52 @@ impl ChatApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_viewport_dimensions(&mut self, height: usize, content_width: usize) {
|
||||||
|
self.viewport_height = height;
|
||||||
|
self.content_width = content_width;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_at_bottom(&self) -> bool {
|
||||||
|
let total_lines = self.calculate_total_content_lines();
|
||||||
|
let max_scroll = total_lines.saturating_sub(self.viewport_height);
|
||||||
|
|
||||||
|
// We're at bottom if scroll is at or near the maximum scroll position
|
||||||
|
self.scroll >= max_scroll || total_lines <= self.viewport_height
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculate_total_content_lines(&self) -> usize {
|
||||||
|
let conversation = self.controller.conversation();
|
||||||
|
let mut formatter = self.controller.formatter().clone();
|
||||||
|
|
||||||
|
// Set the wrap width to match the current display
|
||||||
|
formatter.set_wrap_width(self.content_width);
|
||||||
|
|
||||||
|
let mut total_lines = 0;
|
||||||
|
for (message_index, message) in conversation.messages.iter().enumerate() {
|
||||||
|
let formatted = formatter.format_message(message);
|
||||||
|
let show_role_labels = formatter.show_role_labels();
|
||||||
|
|
||||||
|
// Add role label line if enabled
|
||||||
|
if show_role_labels {
|
||||||
|
total_lines += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add message content lines
|
||||||
|
total_lines += formatted.len();
|
||||||
|
|
||||||
|
// Add empty line after each message, except the last one
|
||||||
|
if message_index < conversation.messages.len() - 1 {
|
||||||
|
total_lines += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minimum 1 line for empty state
|
||||||
|
total_lines.max(1)
|
||||||
|
}
|
||||||
|
|
||||||
fn scroll_to_bottom(&mut self) {
|
fn scroll_to_bottom(&mut self) {
|
||||||
self.scroll = 0;
|
let total_lines = self.calculate_total_content_lines();
|
||||||
|
self.scroll = total_lines.saturating_sub(self.viewport_height);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spawn_stream(&mut self, message_id: Uuid, mut stream: owlen_core::provider::ChatStream) {
|
fn spawn_stream(&mut self, message_id: Uuid, mut stream: owlen_core::provider::ChatStream) {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use unicode_width::UnicodeWidthStr;
|
|||||||
use crate::chat_app::{ChatApp, InputMode};
|
use crate::chat_app::{ChatApp, InputMode};
|
||||||
use owlen_core::types::Role;
|
use owlen_core::types::Role;
|
||||||
|
|
||||||
pub fn render_chat(frame: &mut Frame<'_>, app: &ChatApp) {
|
pub fn render_chat(frame: &mut Frame<'_>, app: &mut ChatApp) {
|
||||||
// Calculate dynamic input height based on textarea content
|
// Calculate dynamic input height based on textarea content
|
||||||
let available_width = frame.area().width;
|
let available_width = frame.area().width;
|
||||||
let input_height = if matches!(app.mode(), InputMode::Editing) {
|
let input_height = if matches!(app.mode(), InputMode::Editing) {
|
||||||
@@ -345,12 +345,16 @@ fn render_header(frame: &mut Frame<'_>, area: Rect, app: &ChatApp) {
|
|||||||
frame.render_widget(paragraph, inner_area);
|
frame.render_widget(paragraph, inner_area);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_messages(frame: &mut Frame<'_>, area: Rect, app: &ChatApp) {
|
fn render_messages(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
|
||||||
|
// Calculate viewport dimensions for autoscroll calculations
|
||||||
|
let viewport_height = area.height.saturating_sub(2) as usize; // subtract borders
|
||||||
|
let content_width = area.width.saturating_sub(4).max(20);
|
||||||
|
app.set_viewport_dimensions(viewport_height, usize::from(content_width));
|
||||||
|
|
||||||
let conversation = app.conversation();
|
let conversation = app.conversation();
|
||||||
let mut formatter = app.formatter().clone();
|
let mut formatter = app.formatter().clone();
|
||||||
|
|
||||||
// Reserve space for borders and the message indent so text fits within the block
|
// Reserve space for borders and the message indent so text fits within the block
|
||||||
let content_width = area.width.saturating_sub(4).max(20);
|
|
||||||
formatter.set_wrap_width(usize::from(content_width));
|
formatter.set_wrap_width(usize::from(content_width));
|
||||||
|
|
||||||
let mut lines: Vec<Line> = Vec::new();
|
let mut lines: Vec<Line> = Vec::new();
|
||||||
|
|||||||
Reference in New Issue
Block a user