Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ pub fn get_save_backup_file_path() -> PathBuf {
pub const ORANGE: ratatui::style::Color = ratatui::style::Color::Rgb(255, 165, 0);
pub const DAEMONIZE_ARG: &str = "__thoth_copy_daemonize";
pub const MIN_TEXTAREA_HEIGHT: usize = 3;
pub const BORDER_PADDING_SIZE: usize = 2;
33 changes: 20 additions & 13 deletions src/scrollable_textarea.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::{
rc::Rc,
};

use crate::{EditorClipboard, MIN_TEXTAREA_HEIGHT};
use crate::{EditorClipboard, BORDER_PADDING_SIZE, MIN_TEXTAREA_HEIGHT};
use crate::{MarkdownRenderer, ORANGE};
use anyhow;
use anyhow::Result;
Expand Down Expand Up @@ -172,11 +172,15 @@ impl ScrollableTextArea {
}

pub fn move_focus(&mut self, direction: isize) {
let new_index = (self.focused_index as isize + direction).max(0) as usize;
if new_index < self.textareas.len() {
self.focused_index = new_index;
self.adjust_scroll_to_focused();
let new_index = self.focused_index as isize + direction;
if new_index >= (self.textareas.len()) as isize {
self.focused_index = 0;
} else if new_index < 0 {
self.focused_index = self.textareas.len() - 1;
} else {
self.focused_index = new_index as usize;
}
self.adjust_scroll_to_focused();
}

pub fn adjust_scroll_to_focused(&mut self) {
Expand All @@ -186,10 +190,10 @@ impl ScrollableTextArea {
let mut height_sum = 0;
for i in self.scroll..=self.focused_index {
let textarea_height =
self.textareas[i].lines().len().max(MIN_TEXTAREA_HEIGHT) as u16 + 2;
self.textareas[i].lines().len().max(MIN_TEXTAREA_HEIGHT) + BORDER_PADDING_SIZE;
height_sum += textarea_height;

if height_sum > self.viewport_height {
if height_sum > self.viewport_height as usize {
self.scroll = i;
break;
}
Expand All @@ -206,7 +210,7 @@ impl ScrollableTextArea {
pub fn calculate_height_to_focused(&self) -> u16 {
self.textareas[self.scroll..=self.focused_index]
.iter()
.map(|ta| ta.lines().len().max(MIN_TEXTAREA_HEIGHT) as u16 + 2)
.map(|ta| (ta.lines().len().max(MIN_TEXTAREA_HEIGHT) + BORDER_PADDING_SIZE) as u16)
.sum()
}

Expand Down Expand Up @@ -279,7 +283,7 @@ impl ScrollableTextArea {
break;
}

let content_height = textarea.lines().len() as u16 + 2;
let content_height = (textarea.lines().len() + BORDER_PADDING_SIZE) as u16;
let is_focused = i == self.focused_index;
let is_editing = is_focused && self.edit_mode;

Expand Down Expand Up @@ -340,7 +344,7 @@ impl ScrollableTextArea {
let rendered_markdown = self.markdown_cache.borrow_mut().get_or_render(
&content,
title,
f.size().width as usize - 2,
f.size().width as usize - BORDER_PADDING_SIZE,
)?;
let paragraph = Paragraph::new(rendered_markdown)
.block(block)
Expand Down Expand Up @@ -404,7 +408,7 @@ impl ScrollableTextArea {
let rendered_markdown = self.markdown_cache.borrow_mut().get_or_render(
&content,
title,
f.size().width as usize - 2,
f.size().width as usize - BORDER_PADDING_SIZE,
)?;

let paragraph = Paragraph::new(rendered_markdown)
Expand Down Expand Up @@ -448,11 +452,14 @@ mod tests {
fn test_move_focus() {
let mut sta = create_test_textarea();
sta.add_textarea(TextArea::default(), "Test1".to_string());
assert_eq!(sta.focused_index, 0);
sta.add_textarea(TextArea::default(), "Test2".to_string());
sta.move_focus(1);

assert_eq!(sta.focused_index, 1);
sta.move_focus(-1);
sta.move_focus(1);
assert_eq!(sta.focused_index, 0);
sta.move_focus(-1);
assert_eq!(sta.focused_index, 1);
}

#[test]
Expand Down
58 changes: 58 additions & 0 deletions src/title_select_popup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ pub struct TitleSelectPopup {
pub titles: Vec<String>,
pub selected_index: usize,
pub visible: bool,
pub scroll_offset: usize,
}

impl TitleSelectPopup {
Expand All @@ -10,6 +11,47 @@ impl TitleSelectPopup {
titles: Vec::new(),
selected_index: 0,
visible: false,
scroll_offset: 0,
}
}

pub fn move_selection_up(&mut self, visible_items: usize) {
if self.titles.is_empty() {
return;
}

if self.selected_index > 0 {
self.selected_index -= 1;
} else {
self.selected_index = self.titles.len() - 1;
}

if self.selected_index < self.scroll_offset {
self.scroll_offset = self.selected_index;
}
if self.selected_index == self.titles.len() - 1 {
self.scroll_offset = self.titles.len().saturating_sub(visible_items);
}
}

pub fn move_selection_down(&mut self, visible_items: usize) {
if self.titles.is_empty() {
return;
}

if self.selected_index < self.titles.len() - 1 {
self.selected_index += 1;
} else {
self.selected_index = 0;
self.scroll_offset = 0;
}

let max_scroll = self.titles.len().saturating_sub(visible_items);
if self.selected_index >= self.scroll_offset + visible_items {
self.scroll_offset = (self.selected_index + 1).saturating_sub(visible_items);
if self.scroll_offset > max_scroll {
self.scroll_offset = max_scroll;
}
}
}
}
Expand Down Expand Up @@ -40,4 +82,20 @@ mod tests {
assert_eq!(popup.titles[0], "Title1");
assert_eq!(popup.titles[1], "Title2");
}

#[test]
fn test_wrap_around_selection() {
let mut popup = TitleSelectPopup::new();
popup.titles = vec!["1".to_string(), "2".to_string(), "3".to_string()];

popup.selected_index = 0;
popup.move_selection_up(2);
assert_eq!(popup.selected_index, 2);
assert_eq!(popup.scroll_offset, 1);

popup.selected_index = 2;
popup.move_selection_down(2);
assert_eq!(popup.selected_index, 0);
assert_eq!(popup.scroll_offset, 0);
}
}
23 changes: 15 additions & 8 deletions src/ui.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{TitlePopup, TitleSelectPopup, ORANGE};
use crate::{TitlePopup, TitleSelectPopup, BORDER_PADDING_SIZE, ORANGE};
use ratatui::{
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
Expand Down Expand Up @@ -67,7 +67,7 @@ pub fn render_edit_commands_popup(f: &mut Frame) {
Cell::from("MAPPINGS").style(Style::default().fg(ORANGE).add_modifier(Modifier::BOLD)),
Cell::from("DESCRIPTIONS").style(Style::default().fg(ORANGE).add_modifier(Modifier::BOLD)),
])
.height(2);
.height(BORDER_PADDING_SIZE as u16);

let commands: Vec<Row> = vec![
Row::new(vec![
Expand Down Expand Up @@ -105,7 +105,7 @@ pub fn render_edit_commands_popup(f: &mut Frame) {
.header(header)
.block(block)
.widths([Constraint::Percentage(30), Constraint::Percentage(70)])
.column_spacing(2)
.column_spacing(BORDER_PADDING_SIZE as u16)
.highlight_style(Style::default().fg(Color::Yellow))
.highlight_symbol(">> ");

Expand Down Expand Up @@ -149,7 +149,7 @@ pub fn render_header(f: &mut Frame, area: Rect, is_edit_mode: bool) {

let thoth_width = thoth.width();
let separator_width = separator.width();
let reserved_width = thoth_width + 2; // 2 extra spaces for padding
let reserved_width = thoth_width + BORDER_PADDING_SIZE; // 2 extra spaces for padding

let mut display_commands = Vec::new();
let mut current_width = 0;
Expand All @@ -166,7 +166,7 @@ pub fn render_header(f: &mut Frame, area: Rect, is_edit_mode: bool) {
let command_string = display_commands.join(separator);
let command_width = command_string.width();

let padding = " ".repeat(available_width - command_width - thoth_width - 2);
let padding = " ".repeat(available_width - command_width - thoth_width - BORDER_PADDING_SIZE);

let header = Line::from(vec![
Span::styled(command_string, Style::default().fg(ORANGE)),
Expand Down Expand Up @@ -200,12 +200,18 @@ pub fn render_title_select_popup(f: &mut Frame, popup: &TitleSelectPopup) {
let area = centered_rect(80, 80, f.size());
f.render_widget(ratatui::widgets::Clear, area);

let items: Vec<Line> = popup
.titles
let visible_height = area.height.saturating_sub(BORDER_PADDING_SIZE as u16) as usize;

let start_idx = popup.scroll_offset;
let end_idx = (popup.scroll_offset + visible_height).min(popup.titles.len());
let visible_titles = &popup.titles[start_idx..end_idx];

let items: Vec<Line> = visible_titles
.iter()
.enumerate()
.map(|(i, title)| {
if i == popup.selected_index {
let absolute_idx = i + popup.scroll_offset;
if absolute_idx == popup.selected_index {
Line::from(vec![Span::styled(
format!("> {}", title),
Style::default().fg(Color::Yellow),
Expand Down Expand Up @@ -374,6 +380,7 @@ mod tests {
titles: vec!["Title1".to_string(), "Title2".to_string()],
selected_index: 0,
visible: true,
scroll_offset: 0,
};

terminal
Expand Down
23 changes: 12 additions & 11 deletions src/ui_handler.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{get_save_backup_file_path, EditorClipboard};
use crate::{get_save_backup_file_path, EditorClipboard, BORDER_PADDING_SIZE};
use anyhow::{bail, Result};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, KeyCode, KeyModifiers},
Expand Down Expand Up @@ -238,6 +238,15 @@ fn handle_title_popup_input(state: &mut UIState, key: event::KeyEvent) -> Result
}

fn handle_title_select_popup_input(state: &mut UIState, key: event::KeyEvent) -> Result<bool> {
// Subtract 2 from viewport height to account for the top and bottom borders
// drawn by Block::default().borders(Borders::ALL) in ui.rs render_title_select_popup.
// The borders are rendered using unicode box-drawing characters:
// top border : ┌───┐
// bottom border : └───┘
// let visible_items = state.scrollable_textarea.viewport_height.saturating_sub(2) as usize;
let visible_items = (state.scrollable_textarea.viewport_height as f32 * 0.8).floor() as usize
- BORDER_PADDING_SIZE;

match key.code {
KeyCode::Enter => {
state
Expand All @@ -250,18 +259,10 @@ fn handle_title_select_popup_input(state: &mut UIState, key: event::KeyEvent) ->
state.edit_commands_popup.visible = false;
}
KeyCode::Up => {
if state.title_select_popup.selected_index > 0 {
state.title_select_popup.selected_index -= 1;
} else {
state.title_select_popup.selected_index = state.title_select_popup.titles.len() - 1
}
state.title_select_popup.move_selection_up(visible_items);
}
KeyCode::Down => {
if state.title_select_popup.selected_index < state.title_select_popup.titles.len() - 1 {
state.title_select_popup.selected_index += 1;
} else {
state.title_select_popup.selected_index = 0;
}
state.title_select_popup.move_selection_down(visible_items);
}
_ => {}
}
Expand Down
4 changes: 3 additions & 1 deletion tests/integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ fn test_full_application_flow() {

// Test focus movement
sta.move_focus(1);
assert_eq!(sta.focused_index, 1);
assert_eq!(sta.focused_index, 0);
sta.move_focus(-1);
assert_eq!(sta.focused_index, 1);
sta.move_focus(1);
assert_eq!(sta.focused_index, 0);

// Test title change
Expand Down
Loading