use ratatui::widgets::{Block, Borders, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState};
use ratatui::style::{Style, Modifier};
use ratatui::Frame;
use ratatui::layout::{Rect, Constraint, Layout, Direction};
use ratatui::text::{Line, Span};
use crate::Config;
use crate::tui::app::{TaskForm, NoteForm, JournalForm, TaskField, NoteField, JournalField};
use crate::tui::widgets::editor::Editor;
use crate::tui::widgets::color::parse_color;
use crate::models::Notebook;
fn wrap_line_with_offsets(line: &str, width: usize) -> Vec<(usize, String)> {
if width == 0 {
return vec![(0, String::new())];
}
if line.chars().count() <= width {
return vec![(0, line.to_string())];
}
let mut wrapped = Vec::new();
let chars: Vec<char> = line.chars().collect();
let mut char_offset = 0;
while char_offset < chars.len() {
let remaining = chars.len() - char_offset;
if remaining <= width {
wrapped.push((char_offset, chars[char_offset..].iter().collect()));
break;
}
let mut break_pos = width;
let search_end = (char_offset + width + 1).min(chars.len());
for i in (char_offset..search_end).rev() {
if i < chars.len() && chars[i].is_whitespace() {
break_pos = i - char_offset + 1;
break;
}
}
let end_pos = char_offset + break_pos;
wrapped.push((char_offset, chars[char_offset..end_pos].iter().collect()));
char_offset = end_pos;
while char_offset < chars.len() && chars[char_offset].is_whitespace() {
char_offset += 1;
}
}
if wrapped.is_empty() {
wrapped.push((0, String::new()));
}
wrapped
}
struct WrappedLineInfo {
logical_line: usize,
char_offset: usize, wrapped_line: String,
}
fn build_all_wrapped_lines(editor_lines: &[String], content_width: usize) -> Vec<WrappedLineInfo> {
let mut all_wrapped = Vec::new();
for (logical_idx, line_str) in editor_lines.iter().enumerate() {
let wrapped = wrap_line_with_offsets(line_str, content_width);
for (char_offset, wrapped_line) in wrapped {
all_wrapped.push(WrappedLineInfo {
logical_line: logical_idx,
char_offset,
wrapped_line,
});
}
}
all_wrapped
}
fn find_cursor_wrapped_line(wrapped_lines: &[WrappedLineInfo], cursor_logical_line: usize, cursor_col: usize) -> usize {
for (idx, wrapped) in wrapped_lines.iter().enumerate() {
if wrapped.logical_line == cursor_logical_line {
let wrapped_line_len = wrapped.wrapped_line.chars().count();
if cursor_col >= wrapped.char_offset && cursor_col < wrapped.char_offset + wrapped_line_len {
return idx;
}
if idx + 1 >= wrapped_lines.len() || wrapped_lines[idx + 1].logical_line != cursor_logical_line {
if cursor_col >= wrapped.char_offset {
return idx;
}
}
}
if wrapped.logical_line > cursor_logical_line {
break;
}
}
wrapped_lines.iter()
.position(|w| w.logical_line == cursor_logical_line)
.unwrap_or(0)
}
fn is_char_selected(
logical_line: usize,
char_offset: usize,
selection_bounds: Option<((usize, usize), (usize, usize))>
) -> bool {
if let Some(((start_line, start_col), (end_line, end_col))) = selection_bounds {
if logical_line < start_line || logical_line > end_line {
return false;
}
if logical_line == start_line && logical_line == end_line {
return char_offset >= start_col && char_offset < end_col;
} else if logical_line == start_line {
return char_offset >= start_col;
} else if logical_line == end_line {
return char_offset < end_col;
} else {
return true;
}
}
false
}
fn build_single_line_with_selection(editor: &Editor, style: Style) -> Line<'static> {
let text = if editor.lines.is_empty() {
String::new()
} else {
editor.lines[0].clone()
};
let selection_bounds = editor.get_selection_bounds();
if selection_bounds.is_none() {
return Line::from(Span::styled(text, style));
}
let mut spans = Vec::new();
let chars: Vec<char> = text.chars().collect();
let mut i = 0;
while i < chars.len() {
let is_selected = is_char_selected(0, i, selection_bounds);
let mut j = i;
while j < chars.len() {
let next_is_selected = is_char_selected(0, j, selection_bounds);
if next_is_selected != is_selected {
break;
}
j += 1;
}
let segment: String = chars[i..j].iter().collect();
let segment_style = if is_selected {
style.add_modifier(Modifier::REVERSED)
} else {
style
};
spans.push(Span::styled(segment, segment_style));
i = j;
}
Line::from(spans)
}
fn build_editor_lines(editor: &Editor, _is_active: bool, style: Style, available_height: u16, available_width: u16) -> Vec<Line<'_>> {
let mut lines = Vec::new();
let editor_lines = editor.lines.clone();
let selection_bounds = editor.get_selection_bounds();
if editor_lines.is_empty() {
lines.push(Line::from(Span::styled("", style)));
} else {
let content_height = available_height.saturating_sub(2) as usize;
let content_width = available_width.saturating_sub(2) as usize;
let all_wrapped = build_all_wrapped_lines(&editor_lines, content_width);
if all_wrapped.is_empty() {
lines.push(Line::from(Span::styled("", style)));
} else {
let cursor_wrapped_line = find_cursor_wrapped_line(&all_wrapped, editor.cursor_line, editor.cursor_col);
let scroll_start = if cursor_wrapped_line < content_height {
0
} else {
cursor_wrapped_line.saturating_sub(content_height - 1)
};
let start_line = scroll_start.min(all_wrapped.len());
let end_line = std::cmp::min(start_line + content_height, all_wrapped.len());
for wrapped_info in all_wrapped[start_line..end_line].iter() {
let logical_line = wrapped_info.logical_line;
let char_offset_start = wrapped_info.char_offset;
let wrapped_text = &wrapped_info.wrapped_line;
if selection_bounds.is_none() {
lines.push(Line::from(Span::styled(wrapped_text.clone(), style)));
} else {
let mut spans = Vec::new();
let chars: Vec<char> = wrapped_text.chars().collect();
let mut i = 0;
while i < chars.len() {
let char_pos = char_offset_start + i;
let is_selected = is_char_selected(logical_line, char_pos, selection_bounds);
let mut j = i;
while j < chars.len() {
let next_char_pos = char_offset_start + j;
let next_is_selected = is_char_selected(logical_line, next_char_pos, selection_bounds);
if next_is_selected != is_selected {
break;
}
j += 1;
}
let segment: String = chars[i..j].iter().collect();
let segment_style = if is_selected {
style.add_modifier(Modifier::REVERSED)
} else {
style
};
spans.push(Span::styled(segment, segment_style));
i = j;
}
lines.push(Line::from(spans));
}
}
if lines.is_empty() {
lines.push(Line::from(Span::styled("", style)));
}
}
}
lines
}
pub fn calculate_field_viewport_height(field_area_height: u16) -> usize {
field_area_height.saturating_sub(2) as usize
}
#[derive(Clone, Copy)]
pub enum FormType {
Task,
Note,
Journal,
}
impl From<&crate::tui::app::CreateForm> for FormType {
fn from(form: &crate::tui::app::CreateForm) -> Self {
match form {
crate::tui::app::CreateForm::Task(_) => FormType::Task,
crate::tui::app::CreateForm::Note(_) => FormType::Note,
crate::tui::app::CreateForm::Journal(_) => FormType::Journal,
}
}
}
pub fn calculate_multi_line_field_height(main_area_height: u16, form_type: FormType) -> u16 {
use ratatui::layout::{Layout, Direction, Constraint};
let (constraints, multi_line_index, single_line_count) = match form_type {
FormType::Task => {
(
vec![
Constraint::Length(3), Constraint::Min(5), Constraint::Length(3), Constraint::Length(3), ],
1, 3, )
}
FormType::Note => {
(
vec![
Constraint::Length(3), Constraint::Length(3), Constraint::Min(5), ],
2, 2, )
}
FormType::Journal => {
(
vec![
Constraint::Length(3), Constraint::Length(3), Constraint::Length(3), Constraint::Min(5), ],
3, 3, )
}
};
let test_area = Rect::new(0, 0, 80, main_area_height); let field_areas = Layout::default()
.direction(Direction::Vertical)
.constraints(constraints)
.split(test_area);
if multi_line_index < field_areas.len() {
field_areas[multi_line_index].height
} else {
let single_line_fields = single_line_count * 3; main_area_height.saturating_sub(single_line_fields).max(5)
}
}
pub fn render_task_form(f: &mut Frame, area: Rect, form: &TaskForm, config: &Config, notebooks: &[Notebook]) {
if area.width < 2 || area.height < 2 {
return;
}
let active_theme = config.get_active_theme();
let highlight_style = Style::default()
.bg(parse_color(&active_theme.highlight_bg))
.fg(parse_color(&active_theme.fg));
let inactive_field_style = Style::default()
.fg(parse_color(&active_theme.fg))
.add_modifier(Modifier::DIM);
let constraints = vec![
Constraint::Length(3), Constraint::Min(5), Constraint::Length(3), Constraint::Length(3), Constraint::Length(3), ];
let field_areas = Layout::default()
.direction(Direction::Vertical)
.constraints(constraints)
.split(area);
let is_title_active = form.current_field == TaskField::Title;
let title_style = if is_title_active { highlight_style } else { inactive_field_style };
let title_line = build_single_line_with_selection(&form.title, title_style);
let title_paragraph = Paragraph::new(title_line)
.block(Block::default().borders(Borders::ALL).title("Title"));
f.render_widget(title_paragraph, field_areas[0]);
let is_desc_active = form.current_field == TaskField::Description;
let desc_style = if is_desc_active { highlight_style } else { inactive_field_style };
let content_height = field_areas[1].height.saturating_sub(2) as usize;
let content_width = field_areas[1].width.saturating_sub(2) as usize;
let all_wrapped = build_all_wrapped_lines(&form.description.lines, content_width);
let total_wrapped_lines = all_wrapped.len();
let cursor_wrapped_line = find_cursor_wrapped_line(&all_wrapped, form.description.cursor_line, form.description.cursor_col);
let vertical_scroll_pos = if cursor_wrapped_line < content_height {
0
} else {
cursor_wrapped_line.saturating_sub(content_height - 1)
};
let needs_vertical_scrollbar = total_wrapped_lines > content_height;
let (desc_area, vertical_scrollbar_area) = if needs_vertical_scrollbar {
let horizontal_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Min(1),
Constraint::Length(1), ])
.split(field_areas[1]);
(horizontal_chunks[0], Some(horizontal_chunks[1]))
} else {
(field_areas[1], None)
};
let desc_lines = build_editor_lines(&form.description, is_desc_active, desc_style, desc_area.height, desc_area.width);
let desc_paragraph = Paragraph::new(desc_lines)
.style(desc_style)
.block(Block::default().borders(Borders::ALL).title("Description/Notes"));
f.render_widget(desc_paragraph, desc_area);
if let Some(v_scrollbar_area) = vertical_scrollbar_area {
if v_scrollbar_area.width > 0 && desc_area.height > 2 {
let scrollbar_inner_area = Rect::new(
v_scrollbar_area.x,
desc_area.y + 1,
v_scrollbar_area.width,
desc_area.height.saturating_sub(2),
);
if scrollbar_inner_area.width > 0 && scrollbar_inner_area.height > 0 {
let mut scrollbar_state = ScrollbarState::new(total_wrapped_lines)
.viewport_content_length(content_height)
.position(vertical_scroll_pos);
let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some("↑"))
.end_symbol(Some("↓"))
.track_symbol(Some("│"))
.thumb_symbol("â–ˆ");
f.render_stateful_widget(scrollbar, scrollbar_inner_area, &mut scrollbar_state);
}
}
}
let is_date_active = form.current_field == TaskField::DueDate;
let date_style = if is_date_active { highlight_style } else { inactive_field_style };
let date_line = build_single_line_with_selection(&form.due_date, date_style);
let date_paragraph = Paragraph::new(date_line)
.block(Block::default().borders(Borders::ALL).title("Due Date (YYYY-MM-DD)"));
f.render_widget(date_paragraph, field_areas[2]);
let is_tags_active = form.current_field == TaskField::Tags;
let tags_style = if is_tags_active { highlight_style } else { inactive_field_style };
let tags_line = build_single_line_with_selection(&form.tags, tags_style);
let tags_paragraph = Paragraph::new(tags_line)
.block(Block::default().borders(Borders::ALL).title("Tags"));
f.render_widget(tags_paragraph, field_areas[3]);
let is_notebook_active = form.current_field == TaskField::Notebook;
let notebook_style = if is_notebook_active { highlight_style } else { inactive_field_style };
let notebook_display = if form.notebook_selected_index == 0 {
"[None]".to_string()
} else {
notebooks.get(form.notebook_selected_index - 1)
.map(|n| n.name.clone())
.unwrap_or_else(|| "[None]".to_string())
};
let notebook_paragraph = Paragraph::new(notebook_display)
.block(Block::default().borders(Borders::ALL).title("Notebook"))
.style(notebook_style);
f.render_widget(notebook_paragraph, field_areas[4]);
if let Some((x, y)) = get_cursor_position_for_task_field(area, form, &field_areas) {
f.set_cursor_position((x, y));
}
}
pub fn render_note_form(f: &mut Frame, area: Rect, form: &NoteForm, config: &Config, notebooks: &[Notebook]) {
if area.width < 2 || area.height < 2 {
return;
}
let active_theme = config.get_active_theme();
let highlight_style = Style::default()
.bg(parse_color(&active_theme.highlight_bg))
.fg(parse_color(&active_theme.fg));
let inactive_field_style = Style::default()
.fg(parse_color(&active_theme.fg))
.add_modifier(Modifier::DIM);
let constraints = vec![
Constraint::Length(3), Constraint::Length(3), Constraint::Length(3), Constraint::Min(5), ];
let field_areas = Layout::default()
.direction(Direction::Vertical)
.constraints(constraints)
.split(area);
let is_title_active = form.current_field == NoteField::Title;
let title_style = if is_title_active { highlight_style } else { inactive_field_style };
let title_line = build_single_line_with_selection(&form.title, title_style);
let title_paragraph = Paragraph::new(title_line)
.block(Block::default().borders(Borders::ALL).title("Title"));
f.render_widget(title_paragraph, field_areas[0]);
let is_tags_active = form.current_field == NoteField::Tags;
let tags_style = if is_tags_active { highlight_style } else { inactive_field_style };
let tags_line = build_single_line_with_selection(&form.tags, tags_style);
let tags_paragraph = Paragraph::new(tags_line)
.block(Block::default().borders(Borders::ALL).title("Tags"));
f.render_widget(tags_paragraph, field_areas[1]);
let is_notebook_active = form.current_field == NoteField::Notebook;
let notebook_style = if is_notebook_active { highlight_style } else { inactive_field_style };
let notebook_display = if form.notebook_selected_index == 0 {
"[None]".to_string()
} else {
notebooks.get(form.notebook_selected_index - 1)
.map(|n| n.name.clone())
.unwrap_or_else(|| "[None]".to_string())
};
let notebook_paragraph = Paragraph::new(notebook_display)
.block(Block::default().borders(Borders::ALL).title("Notebook"))
.style(notebook_style);
f.render_widget(notebook_paragraph, field_areas[2]);
let is_content_active = form.current_field == NoteField::Content;
let content_style = if is_content_active { highlight_style } else { inactive_field_style };
let content_height = field_areas[3].height.saturating_sub(2) as usize;
let content_width = field_areas[3].width.saturating_sub(2) as usize;
let all_wrapped = build_all_wrapped_lines(&form.content.lines, content_width);
let total_wrapped_lines = all_wrapped.len();
let cursor_wrapped_line = find_cursor_wrapped_line(&all_wrapped, form.content.cursor_line, form.content.cursor_col);
let vertical_scroll_pos = if cursor_wrapped_line < content_height {
0
} else {
cursor_wrapped_line.saturating_sub(content_height - 1)
};
let needs_vertical_scrollbar = total_wrapped_lines > content_height;
let (content_area, vertical_scrollbar_area) = if needs_vertical_scrollbar {
let horizontal_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Min(1),
Constraint::Length(1), ])
.split(field_areas[3]);
(horizontal_chunks[0], Some(horizontal_chunks[1]))
} else {
(field_areas[3], None)
};
let content_lines = build_editor_lines(&form.content, is_content_active, content_style, content_area.height, content_area.width);
let content_paragraph = Paragraph::new(content_lines)
.style(content_style)
.block(Block::default().borders(Borders::ALL).title("Content"));
f.render_widget(content_paragraph, content_area);
if let Some(v_scrollbar_area) = vertical_scrollbar_area {
if v_scrollbar_area.width > 0 && content_area.height > 2 {
let scrollbar_inner_area = Rect::new(
v_scrollbar_area.x,
content_area.y + 1,
v_scrollbar_area.width,
content_area.height.saturating_sub(2),
);
if scrollbar_inner_area.width > 0 && scrollbar_inner_area.height > 0 {
let mut scrollbar_state = ScrollbarState::new(total_wrapped_lines)
.viewport_content_length(content_height)
.position(vertical_scroll_pos);
let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some("↑"))
.end_symbol(Some("↓"))
.track_symbol(Some("│"))
.thumb_symbol("â–ˆ");
f.render_stateful_widget(scrollbar, scrollbar_inner_area, &mut scrollbar_state);
}
}
}
if let Some((x, y)) = get_cursor_position_for_note_field(area, form, &field_areas) {
f.set_cursor_position((x, y));
}
}
pub fn render_journal_form(f: &mut Frame, area: Rect, form: &JournalForm, config: &Config, notebooks: &[Notebook]) {
if area.width < 2 || area.height < 2 {
return;
}
let active_theme = config.get_active_theme();
let highlight_style = Style::default()
.bg(parse_color(&active_theme.highlight_bg))
.fg(parse_color(&active_theme.fg));
let inactive_field_style = Style::default()
.fg(parse_color(&active_theme.fg))
.add_modifier(Modifier::DIM);
let constraints = vec![
Constraint::Length(3), Constraint::Length(3), Constraint::Length(3), Constraint::Length(3), Constraint::Min(5), ];
let field_areas = Layout::default()
.direction(Direction::Vertical)
.constraints(constraints)
.split(area);
let is_date_active = form.current_field == JournalField::Date;
let date_style = if is_date_active { highlight_style } else { inactive_field_style };
let date_line = build_single_line_with_selection(&form.date, date_style);
let date_paragraph = Paragraph::new(date_line)
.block(Block::default().borders(Borders::ALL).title("Date (YYYY-MM-DD)"));
f.render_widget(date_paragraph, field_areas[0]);
let is_title_active = form.current_field == JournalField::Title;
let title_style = if is_title_active { highlight_style } else { inactive_field_style };
let title_line = build_single_line_with_selection(&form.title, title_style);
let title_paragraph = Paragraph::new(title_line)
.block(Block::default().borders(Borders::ALL).title("Title"));
f.render_widget(title_paragraph, field_areas[1]);
let is_tags_active = form.current_field == JournalField::Tags;
let tags_style = if is_tags_active { highlight_style } else { inactive_field_style };
let tags_line = build_single_line_with_selection(&form.tags, tags_style);
let tags_paragraph = Paragraph::new(tags_line)
.block(Block::default().borders(Borders::ALL).title("Tags"));
f.render_widget(tags_paragraph, field_areas[2]);
let is_notebook_active = form.current_field == JournalField::Notebook;
let notebook_style = if is_notebook_active { highlight_style } else { inactive_field_style };
let notebook_display = if form.notebook_selected_index == 0 {
"[None]".to_string()
} else {
notebooks.get(form.notebook_selected_index - 1)
.map(|n| n.name.clone())
.unwrap_or_else(|| "[None]".to_string())
};
let notebook_paragraph = Paragraph::new(notebook_display)
.block(Block::default().borders(Borders::ALL).title("Notebook"))
.style(notebook_style);
f.render_widget(notebook_paragraph, field_areas[3]);
let is_content_active = form.current_field == JournalField::Content;
let content_style = if is_content_active { highlight_style } else { inactive_field_style };
let content_height = field_areas[4].height.saturating_sub(2) as usize;
let content_width = field_areas[4].width.saturating_sub(2) as usize;
let all_wrapped = build_all_wrapped_lines(&form.content.lines, content_width);
let total_wrapped_lines = all_wrapped.len();
let cursor_wrapped_line = find_cursor_wrapped_line(&all_wrapped, form.content.cursor_line, form.content.cursor_col);
let vertical_scroll_pos = if cursor_wrapped_line < content_height {
0
} else {
cursor_wrapped_line.saturating_sub(content_height - 1)
};
let needs_vertical_scrollbar = total_wrapped_lines > content_height;
let (content_area, vertical_scrollbar_area) = if needs_vertical_scrollbar {
let horizontal_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Min(1),
Constraint::Length(1), ])
.split(field_areas[4]);
(horizontal_chunks[0], Some(horizontal_chunks[1]))
} else {
(field_areas[4], None)
};
let content_lines = build_editor_lines(&form.content, is_content_active, content_style, content_area.height, content_area.width);
let content_paragraph = Paragraph::new(content_lines)
.style(content_style)
.block(Block::default().borders(Borders::ALL).title("Content"));
f.render_widget(content_paragraph, content_area);
if let Some(v_scrollbar_area) = vertical_scrollbar_area {
if v_scrollbar_area.width > 0 && content_area.height > 2 {
let scrollbar_inner_area = Rect::new(
v_scrollbar_area.x,
content_area.y + 1,
v_scrollbar_area.width,
content_area.height.saturating_sub(2),
);
if scrollbar_inner_area.width > 0 && scrollbar_inner_area.height > 0 {
let mut scrollbar_state = ScrollbarState::new(total_wrapped_lines)
.viewport_content_length(content_height)
.position(vertical_scroll_pos);
let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some("↑"))
.end_symbol(Some("↓"))
.track_symbol(Some("│"))
.thumb_symbol("â–ˆ");
f.render_stateful_widget(scrollbar, scrollbar_inner_area, &mut scrollbar_state);
}
}
}
if let Some((x, y)) = get_cursor_position_for_journal_field(area, form, &field_areas) {
f.set_cursor_position((x, y));
}
}
fn get_cursor_position_for_task_field(_area: Rect, form: &TaskForm, field_areas: &[Rect]) -> Option<(u16, u16)> {
let editor = match form.current_field {
TaskField::Title => &form.title,
TaskField::Description => &form.description,
TaskField::DueDate => &form.due_date,
TaskField::Tags => &form.tags,
TaskField::Notebook => return None, };
let field_index = match form.current_field {
TaskField::Title => 0,
TaskField::Description => 1,
TaskField::DueDate => 2,
TaskField::Tags => 3,
TaskField::Notebook => return None,
};
if field_index >= field_areas.len() {
return None;
}
let field_area = field_areas[field_index];
if form.current_field == TaskField::Description {
let content_width = field_area.width.saturating_sub(2) as usize;
let content_height = field_area.height.saturating_sub(2) as usize;
let all_wrapped = build_all_wrapped_lines(&editor.lines, content_width);
if all_wrapped.is_empty() {
return Some((field_area.x + 1, field_area.y + 1));
}
let cursor_wrapped_idx = find_cursor_wrapped_line(&all_wrapped, editor.cursor_line, editor.cursor_col);
let scroll_start = if cursor_wrapped_idx < content_height {
0
} else {
cursor_wrapped_idx.saturating_sub(content_height - 1)
};
let visible_wrapped_idx = cursor_wrapped_idx.saturating_sub(scroll_start);
if visible_wrapped_idx >= content_height {
return None; }
if cursor_wrapped_idx >= all_wrapped.len() {
return None;
}
let wrapped_info = &all_wrapped[cursor_wrapped_idx];
let col_in_wrapped = editor.cursor_col.saturating_sub(wrapped_info.char_offset);
let wrapped_line_len = wrapped_info.wrapped_line.chars().count();
let cursor_col = col_in_wrapped.min(wrapped_line_len);
let x = field_area.x + 1 + (cursor_col.min((field_area.width.saturating_sub(2)) as usize) as u16);
let y = field_area.y + 1 + (visible_wrapped_idx.min((field_area.height.saturating_sub(2)) as usize) as u16);
if x < field_area.x + field_area.width && y < field_area.y + field_area.height {
Some((x, y))
} else {
None
}
} else {
let cursor_col = editor.cursor_col;
let x = field_area.x + 1 + (cursor_col.min((field_area.width.saturating_sub(2)) as usize) as u16);
let y = field_area.y + 1;
if x < field_area.x + field_area.width && y < field_area.y + field_area.height {
Some((x, y))
} else {
None
}
}
}
fn get_cursor_position_for_note_field(_area: Rect, form: &NoteForm, field_areas: &[Rect]) -> Option<(u16, u16)> {
let editor = match form.current_field {
NoteField::Title => &form.title,
NoteField::Tags => &form.tags,
NoteField::Notebook => return None, NoteField::Content => &form.content,
};
let field_index = match form.current_field {
NoteField::Title => 0,
NoteField::Tags => 1,
NoteField::Notebook => return None,
NoteField::Content => 3,
};
if field_index >= field_areas.len() {
return None;
}
let field_area = field_areas[field_index];
if form.current_field == NoteField::Content {
let content_width = field_area.width.saturating_sub(2) as usize;
let content_height = field_area.height.saturating_sub(2) as usize;
let all_wrapped = build_all_wrapped_lines(&editor.lines, content_width);
if all_wrapped.is_empty() {
return Some((field_area.x + 1, field_area.y + 1));
}
let cursor_wrapped_idx = find_cursor_wrapped_line(&all_wrapped, editor.cursor_line, editor.cursor_col);
let scroll_start = if cursor_wrapped_idx < content_height {
0
} else {
cursor_wrapped_idx.saturating_sub(content_height - 1)
};
let visible_wrapped_idx = cursor_wrapped_idx.saturating_sub(scroll_start);
if visible_wrapped_idx >= content_height {
return None; }
if cursor_wrapped_idx >= all_wrapped.len() {
return None;
}
let wrapped_info = &all_wrapped[cursor_wrapped_idx];
let col_in_wrapped = editor.cursor_col.saturating_sub(wrapped_info.char_offset);
let wrapped_line_len = wrapped_info.wrapped_line.chars().count();
let cursor_col = col_in_wrapped.min(wrapped_line_len);
let x = field_area.x + 1 + (cursor_col.min((field_area.width.saturating_sub(2)) as usize) as u16);
let y = field_area.y + 1 + (visible_wrapped_idx.min((field_area.height.saturating_sub(2)) as usize) as u16);
if x < field_area.x + field_area.width && y < field_area.y + field_area.height {
Some((x, y))
} else {
None
}
} else {
let cursor_col = editor.cursor_col;
let x = field_area.x + 1 + (cursor_col.min((field_area.width.saturating_sub(2)) as usize) as u16);
let y = field_area.y + 1;
if x < field_area.x + field_area.width && y < field_area.y + field_area.height {
Some((x, y))
} else {
None
}
}
}
fn get_cursor_position_for_journal_field(_area: Rect, form: &JournalForm, field_areas: &[Rect]) -> Option<(u16, u16)> {
let editor = match form.current_field {
JournalField::Date => &form.date,
JournalField::Title => &form.title,
JournalField::Tags => &form.tags,
JournalField::Notebook => return None, JournalField::Content => &form.content,
};
let field_index = match form.current_field {
JournalField::Date => 0,
JournalField::Title => 1,
JournalField::Tags => 2,
JournalField::Notebook => return None,
JournalField::Content => 4,
};
if field_index >= field_areas.len() {
return None;
}
let field_area = field_areas[field_index];
if form.current_field == JournalField::Content {
let content_width = field_area.width.saturating_sub(2) as usize;
let content_height = field_area.height.saturating_sub(2) as usize;
let all_wrapped = build_all_wrapped_lines(&editor.lines, content_width);
if all_wrapped.is_empty() {
return Some((field_area.x + 1, field_area.y + 1));
}
let cursor_wrapped_idx = find_cursor_wrapped_line(&all_wrapped, editor.cursor_line, editor.cursor_col);
let scroll_start = if cursor_wrapped_idx < content_height {
0
} else {
cursor_wrapped_idx.saturating_sub(content_height - 1)
};
let visible_wrapped_idx = cursor_wrapped_idx.saturating_sub(scroll_start);
if visible_wrapped_idx >= content_height {
return None; }
if cursor_wrapped_idx >= all_wrapped.len() {
return None;
}
let wrapped_info = &all_wrapped[cursor_wrapped_idx];
let col_in_wrapped = editor.cursor_col.saturating_sub(wrapped_info.char_offset);
let wrapped_line_len = wrapped_info.wrapped_line.chars().count();
let cursor_col = col_in_wrapped.min(wrapped_line_len);
let x = field_area.x + 1 + (cursor_col.min((field_area.width.saturating_sub(2)) as usize) as u16);
let y = field_area.y + 1 + (visible_wrapped_idx.min((field_area.height.saturating_sub(2)) as usize) as u16);
if x < field_area.x + field_area.width && y < field_area.y + field_area.height {
Some((x, y))
} else {
None
}
} else {
let cursor_col = editor.cursor_col;
let x = field_area.x + 1 + (cursor_col.min((field_area.width.saturating_sub(2)) as usize) as u16);
let y = field_area.y + 1;
if x < field_area.x + field_area.width && y < field_area.y + field_area.height {
Some((x, y))
} else {
None
}
}
}