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:
Ruben Rosario
2026-06-21 18:01:49 +01:00
parent 822c335864
commit 7ebafec3c0
2 changed files with 104 additions and 71 deletions
+91 -55
View File
@@ -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
View File
@@ -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 {