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