Calendar panel: full-width layout with 4 weekly columns
- Moved Calendar from body left column (8 lines) to full-width row between body and status bar (12 lines) - Calendar splits into 4 horizontal panels, each showing one week starting from Monday of the current week - Day headers in Cyan (Yellow for today), events in White - Removed old date-grouped event list rendering - Body layout simplified to single horizontal split (Tasks | Detail)
This commit is contained in:
+91
-55
@@ -1,3 +1,5 @@
|
||||
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};
|
||||
@@ -600,70 +602,104 @@ pub fn render_calendar_panel(
|
||||
area: Rect,
|
||||
events: &[CalendarEvent],
|
||||
focused: bool,
|
||||
scroll: u16,
|
||||
_scroll: u16,
|
||||
) {
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(if focused { FOCUS_COLOR } else { Color::DarkGray }))
|
||||
.title(" Calendar ")
|
||||
.title_alignment(Alignment::Left);
|
||||
|
||||
if events.is_empty() {
|
||||
let paragraph = Paragraph::new(Text::from(Line::from(Span::styled(
|
||||
" No upcoming events ",
|
||||
Style::default().fg(Color::DarkGray),
|
||||
))))
|
||||
.block(block)
|
||||
.alignment(Alignment::Center);
|
||||
frame.render_widget(paragraph, area);
|
||||
if area.width < 20 || area.height < 3 {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut lines: Vec<Line> = Vec::new();
|
||||
let mut current_date: Option<chrono::NaiveDate> = None;
|
||||
let today = chrono::Local::now().naive_local().date();
|
||||
let weekday = today.weekday().num_days_from_monday();
|
||||
let this_monday = today - chrono::Duration::days(weekday as i64);
|
||||
|
||||
for event in events {
|
||||
if let Some(start) = event.start {
|
||||
let event_date = start.date();
|
||||
if Some(event_date) != current_date {
|
||||
current_date = Some(event_date);
|
||||
let day_header = format!(
|
||||
" --- {} {} --- ",
|
||||
event_date.format("%A"),
|
||||
event_date.format("%d/%m"),
|
||||
);
|
||||
lines.push(Line::from(Span::styled(
|
||||
day_header,
|
||||
Style::default().fg(Color::Gray),
|
||||
)));
|
||||
let cols = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Ratio(1, 4); 4])
|
||||
.split(area);
|
||||
|
||||
let has_events = events.iter().any(|e| {
|
||||
e.start.map_or(false, |s| {
|
||||
let d = s.date();
|
||||
d >= this_monday && d < this_monday + chrono::Duration::days(28)
|
||||
})
|
||||
});
|
||||
|
||||
for week_idx in 0..4 {
|
||||
let week_start = this_monday + chrono::Duration::weeks(week_idx as i64);
|
||||
let week_title = format!(" W/C {} ", week_start.format("%d/%m"));
|
||||
let col_area = cols[week_idx];
|
||||
|
||||
let border = if focused { FOCUS_COLOR } else { Color::DarkGray };
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(border))
|
||||
.title(week_title)
|
||||
.title_alignment(Alignment::Left);
|
||||
|
||||
if !has_events {
|
||||
let msg = if week_idx == 0 {
|
||||
" No upcoming events "
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let paragraph = Paragraph::new(Text::from(Line::from(Span::styled(
|
||||
msg,
|
||||
Style::default().fg(Color::DarkGray),
|
||||
))))
|
||||
.block(block);
|
||||
frame.render_widget(paragraph, col_area);
|
||||
continue;
|
||||
}
|
||||
|
||||
let inner = block.inner(col_area);
|
||||
let inner_h = inner.height as usize;
|
||||
|
||||
let mut lines: Vec<Line> = Vec::new();
|
||||
|
||||
for day_offset in 0..7 {
|
||||
let day = week_start + chrono::Duration::days(day_offset);
|
||||
|
||||
let day_style = if day == today {
|
||||
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)
|
||||
};
|
||||
let day_label = format!(
|
||||
" {} {}",
|
||||
match day.weekday() {
|
||||
chrono::Weekday::Mon => "Mon",
|
||||
chrono::Weekday::Tue => "Tue",
|
||||
chrono::Weekday::Wed => "Wed",
|
||||
chrono::Weekday::Thu => "Thu",
|
||||
chrono::Weekday::Fri => "Fri",
|
||||
chrono::Weekday::Sat => "Sat",
|
||||
chrono::Weekday::Sun => "Sun",
|
||||
},
|
||||
day.format("%d/%m")
|
||||
);
|
||||
lines.push(Line::from(Span::styled(day_label, day_style)));
|
||||
|
||||
if lines.len() >= inner_h {
|
||||
break;
|
||||
}
|
||||
|
||||
let time_str = start.format("%H:%M").to_string();
|
||||
let summary = &event.summary;
|
||||
let line_text = if summary.len() > 30 {
|
||||
format!(" {} {:.30}…", time_str, summary)
|
||||
} else {
|
||||
format!(" {} {}", time_str, summary)
|
||||
};
|
||||
lines.push(Line::from(Span::styled(
|
||||
line_text,
|
||||
Style::default().fg(DETAIL_COLOR),
|
||||
)));
|
||||
for event in events.iter().filter(|e| e.start.map_or(false, |s| s.date() == day)) {
|
||||
let time_str = event.start.map(|s| s.format("%H:%M").to_string()).unwrap_or_default();
|
||||
let line_text = format!(" {} {}", time_str, event.summary);
|
||||
lines.push(Line::from(Span::styled(
|
||||
line_text,
|
||||
Style::default().fg(DETAIL_COLOR),
|
||||
)));
|
||||
|
||||
if lines.len() >= inner_h {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let paragraph = Paragraph::new(Text::from(lines)).block(block);
|
||||
frame.render_widget(paragraph, col_area);
|
||||
}
|
||||
|
||||
let inner_h = (area.height as usize).saturating_sub(2);
|
||||
let visible_lines: Vec<Line> = lines
|
||||
.iter()
|
||||
.skip(scroll as usize)
|
||||
.take(inner_h)
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
let paragraph = Paragraph::new(Text::from(visible_lines))
|
||||
.block(block)
|
||||
.scroll((0, 0));
|
||||
frame.render_widget(paragraph, area);
|
||||
}
|
||||
|
||||
/// Simple word wrap: splits text at word boundaries to fit max_width chars per line
|
||||
|
||||
+13
-16
@@ -61,13 +61,15 @@ pub fn draw(frame: &mut Frame, view: AppView) {
|
||||
.constraints([
|
||||
Constraint::Length(3),
|
||||
Constraint::Min(0),
|
||||
Constraint::Length(12),
|
||||
Constraint::Length(1),
|
||||
])
|
||||
.split(area);
|
||||
|
||||
let tabs_area = main_layout[0];
|
||||
let body_area = main_layout[1];
|
||||
let status_area = main_layout[2];
|
||||
let calendar_area = main_layout[2];
|
||||
let status_area = main_layout[3];
|
||||
|
||||
let is_tabs_focused = view.focus == Focus::Tabs;
|
||||
render_tabs_bar(frame, tabs_area, view.lists, view.selected_list, is_tabs_focused);
|
||||
@@ -77,30 +79,16 @@ pub fn draw(frame: &mut Frame, view: AppView) {
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.split(body_area);
|
||||
|
||||
let left_col = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Min(0), Constraint::Length(8)])
|
||||
.split(body_layout[0]);
|
||||
|
||||
let is_task_list_focused = view.focus == Focus::TaskList;
|
||||
render_task_list(
|
||||
frame,
|
||||
left_col[0],
|
||||
body_layout[0],
|
||||
view.tasks,
|
||||
view.selected_task,
|
||||
is_task_list_focused,
|
||||
view.task_list_scroll,
|
||||
);
|
||||
|
||||
let is_calendar_focused = view.focus == Focus::Calendar;
|
||||
render_calendar_panel(
|
||||
frame,
|
||||
left_col[1],
|
||||
view.calendar_events,
|
||||
is_calendar_focused,
|
||||
view.calendar_scroll,
|
||||
);
|
||||
|
||||
let is_detail_focused = view.focus == Focus::Detail;
|
||||
render_detail(
|
||||
frame,
|
||||
@@ -110,6 +98,15 @@ pub fn draw(frame: &mut Frame, view: AppView) {
|
||||
view.detail_scroll,
|
||||
);
|
||||
|
||||
let is_calendar_focused = view.focus == Focus::Calendar;
|
||||
render_calendar_panel(
|
||||
frame,
|
||||
calendar_area,
|
||||
view.calendar_events,
|
||||
is_calendar_focused,
|
||||
view.calendar_scroll,
|
||||
);
|
||||
|
||||
render_status_bar(frame, status_area, view.network_status, view.sync_stats);
|
||||
|
||||
if let Some(popup) = view.show_popup {
|
||||
|
||||
Reference in New Issue
Block a user