feat: integrar painel de listas no painel de tarefas
- Remove Focus::Tabs, tabs das listas passam para dentro do painel esquerdo - Layout simplificado: top area eliminada, body/calendar/status apenas - Navegação entre listas com ←/→ no foco TaskList - Tabs dinâmicas: separador '|', alinhamento à esquerda, número variável conforme largura do terminal - Indicadores ' < ' e '>>' para overflow horizontal - Scroll horizontal (list_tabs_scroll) com clamping automático
This commit is contained in:
+92
-42
@@ -4,7 +4,7 @@ use chrono::Datelike;
|
||||
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use ratatui::text::{Line, Span, Text};
|
||||
use ratatui::widgets::{Block, Borders, Clear, List, ListItem, Paragraph, Tabs, Wrap};
|
||||
use ratatui::widgets::{Block, Borders, Clear, List, ListItem, Paragraph, Wrap};
|
||||
use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect};
|
||||
use ratatui::Frame;
|
||||
|
||||
@@ -12,7 +12,6 @@ use crate::domain::models::*;
|
||||
use crate::app::SyncStats;
|
||||
use super::NetworkStatus;
|
||||
|
||||
const TAB_COLOR: Color = Color::Cyan;
|
||||
const FOCUS_COLOR: Color = Color::Yellow;
|
||||
const SELECTED_COLOR: Color = Color::Green;
|
||||
const DETAIL_COLOR: Color = Color::White;
|
||||
@@ -22,37 +21,6 @@ const STATUS_SYNC: Color = Color::Yellow;
|
||||
const POPUP_BG: Color = Color::Black;
|
||||
const POPUP_BORDER: Color = Color::Cyan;
|
||||
|
||||
pub fn render_tabs_bar(
|
||||
frame: &mut Frame,
|
||||
area: Rect,
|
||||
lists: &[TaskList],
|
||||
selected: usize,
|
||||
focused: bool,
|
||||
) {
|
||||
let tab_titles: Vec<&str> = lists.iter().map(|l| l.title.as_str()).collect();
|
||||
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(if focused { FOCUS_COLOR } else { TAB_COLOR }))
|
||||
.title(" Lists ")
|
||||
.title_alignment(Alignment::Left);
|
||||
|
||||
let tabs = if tab_titles.is_empty() {
|
||||
Tabs::new(vec![Line::from(" No lists ")])
|
||||
.block(block)
|
||||
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
|
||||
.select(selected)
|
||||
} else {
|
||||
Tabs::new(tab_titles.into_iter().map(|t| Line::from(Span::raw(t))).collect::<Vec<_>>())
|
||||
.block(block)
|
||||
.divider(Span::raw(" | "))
|
||||
.highlight_style(Style::default().fg(FOCUS_COLOR).add_modifier(Modifier::BOLD))
|
||||
.select(selected)
|
||||
};
|
||||
|
||||
frame.render_widget(tabs, area);
|
||||
}
|
||||
|
||||
fn relative_due_str(due: chrono::NaiveDateTime) -> (String, Color) {
|
||||
let now = chrono::Local::now().naive_local();
|
||||
let diff = due - now;
|
||||
@@ -74,6 +42,9 @@ fn relative_due_str(due: chrono::NaiveDateTime) -> (String, Color) {
|
||||
pub fn render_task_list(
|
||||
frame: &mut Frame,
|
||||
area: Rect,
|
||||
lists: &[TaskList],
|
||||
selected_list: usize,
|
||||
list_tabs_scroll: usize,
|
||||
tasks: &[Task],
|
||||
selected: usize,
|
||||
focused: bool,
|
||||
@@ -84,7 +55,26 @@ pub fn render_task_list(
|
||||
let done = tasks.iter().filter(|t| t.status == TaskStatus::Completed).count();
|
||||
let todo = total - done;
|
||||
|
||||
let content_width = (area.width as usize).saturating_sub(5);
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(if focused { FOCUS_COLOR } else { Color::DarkGray }))
|
||||
.title(format!(" Tasks ({} todo / {} done) ", todo, done))
|
||||
.title_alignment(Alignment::Left);
|
||||
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(&block, area);
|
||||
|
||||
let inner_split = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Length(3), Constraint::Min(0)])
|
||||
.split(inner);
|
||||
|
||||
let tabs_area = inner_split[0];
|
||||
let tasks_area = inner_split[1];
|
||||
|
||||
render_list_tabs(frame, tabs_area, lists, selected_list, list_tabs_scroll, focused);
|
||||
|
||||
let content_width = (tasks_area.width as usize).saturating_sub(1);
|
||||
|
||||
let items: Vec<ListItem> = tasks
|
||||
.iter()
|
||||
@@ -160,14 +150,8 @@ pub fn render_task_list(
|
||||
})
|
||||
.collect();
|
||||
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(if focused { FOCUS_COLOR } else { Color::DarkGray }))
|
||||
.title(format!(" Tasks ({} todo / {} done) ", todo, done))
|
||||
.title_alignment(Alignment::Left);
|
||||
|
||||
let list = List::new(items)
|
||||
.block(block)
|
||||
.block(Block::default())
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.bg(if focused { Color::DarkGray } else { Color::Black })
|
||||
@@ -176,7 +160,73 @@ pub fn render_task_list(
|
||||
)
|
||||
.highlight_symbol(">> ");
|
||||
|
||||
frame.render_stateful_widget(list, area, &mut ratatui::widgets::ListState::default().with_selected(Some(selected)));
|
||||
frame.render_stateful_widget(list, tasks_area, &mut ratatui::widgets::ListState::default().with_selected(Some(selected)));
|
||||
}
|
||||
|
||||
fn render_list_tabs(
|
||||
frame: &mut Frame,
|
||||
area: Rect,
|
||||
lists: &[TaskList],
|
||||
selected_list: usize,
|
||||
scroll: usize,
|
||||
focused: bool,
|
||||
) {
|
||||
if lists.is_empty() || area.width < 4 {
|
||||
return;
|
||||
}
|
||||
|
||||
let max_width = area.width as usize;
|
||||
let mut spans: Vec<Span> = Vec::new();
|
||||
|
||||
let mut used = if scroll > 0 {
|
||||
spans.push(Span::styled(" <", Style::default().fg(Color::DarkGray)));
|
||||
2
|
||||
} else {
|
||||
spans.push(Span::raw(" "));
|
||||
1
|
||||
};
|
||||
|
||||
let mut last_idx = scroll.wrapping_sub(1);
|
||||
for i in scroll..lists.len() {
|
||||
let title = &lists[i].title;
|
||||
let title_len = title.chars().count();
|
||||
let need_sep = if i > scroll { 3 } else { 0 };
|
||||
let tab_space = title_len + 2;
|
||||
|
||||
if used + need_sep + tab_space > max_width {
|
||||
break;
|
||||
}
|
||||
|
||||
if need_sep > 0 {
|
||||
spans.push(Span::raw(" | "));
|
||||
used += 3;
|
||||
}
|
||||
|
||||
let is_selected = i == selected_list;
|
||||
let style = if is_selected && focused {
|
||||
Style::default().fg(FOCUS_COLOR).add_modifier(Modifier::BOLD)
|
||||
} else if is_selected {
|
||||
Style::default().fg(FOCUS_COLOR)
|
||||
} else {
|
||||
Style::default().fg(Color::Cyan)
|
||||
};
|
||||
|
||||
spans.push(Span::styled(format!(" {} ", title), style));
|
||||
used += title_len + 2;
|
||||
last_idx = i;
|
||||
}
|
||||
|
||||
if last_idx + 1 < lists.len() && used + 5 <= max_width {
|
||||
spans.push(Span::raw(" | "));
|
||||
spans.push(Span::styled(">>", Style::default().fg(Color::DarkGray)));
|
||||
}
|
||||
|
||||
if used < max_width {
|
||||
spans.push(Span::raw(" ".repeat(max_width - used)));
|
||||
}
|
||||
|
||||
let paragraph = Paragraph::new(Line::from(spans));
|
||||
frame.render_widget(paragraph, area);
|
||||
}
|
||||
|
||||
pub fn render_detail(
|
||||
|
||||
Reference in New Issue
Block a user