Implement side-by-side rendering for overlapping events in week view

- Add overlap detection algorithm to identify overlapping events
- Implement layout calculation to arrange events in columns
- Update event positioning to use dynamic left/width instead of fixed right
- Events now render side-by-side when they overlap in time
- Maintains proper spacing and margins for all event arrangements

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Connor Johnstone
2025-09-02 10:44:32 -04:00
parent 6c67444b19
commit 7c2901f453
2 changed files with 104 additions and 9 deletions

View File

@@ -353,6 +353,7 @@ pub fn week_view(props: &WeekViewProps) -> Html {
week_days.iter().enumerate().map(|(_column_index, date)| {
let is_today = *date == props.today;
let day_events = props.events.get(date).cloned().unwrap_or_default();
let event_layouts = calculate_event_layout(&day_events, *date);
// Drag event handlers
let drag_state_clone = drag_state.clone();
@@ -589,7 +590,7 @@ pub fn week_view(props: &WeekViewProps) -> Html {
// Events positioned absolutely based on their actual times
<div class="events-container">
{
day_events.iter().filter_map(|event| {
day_events.iter().enumerate().filter_map(|(event_idx, event)| {
let (start_pixels, duration_pixels, is_all_day) = calculate_event_position(event, *date);
// Skip events that don't belong on this date or have invalid positioning
@@ -782,12 +783,28 @@ pub fn week_view(props: &WeekViewProps) -> Html {
if is_refreshing { Some("refreshing") } else { None },
if is_all_day { Some("all-day") } else { None }
)}
style={format!(
"background-color: {}; top: {}px; height: {}px;",
event_color,
start_pixels,
duration_pixels
)}
style={
let (column_idx, total_columns) = event_layouts[event_idx];
let column_width = if total_columns > 1 {
format!("calc((100% - 8px) / {})", total_columns) // Account for 4px margins on each side
} else {
"calc(100% - 8px)".to_string()
};
let left_offset = if total_columns > 1 {
format!("calc(4px + {} * (100% - 8px) / {})", column_idx, total_columns)
} else {
"4px".to_string()
};
format!(
"background-color: {}; top: {}px; height: {}px; left: {}; width: {}; right: auto;",
event_color,
start_pixels,
duration_pixels,
left_offset,
column_width
)
}
{onclick}
{oncontextmenu}
onmousedown={onmousedown_event}
@@ -1065,3 +1082,82 @@ fn calculate_event_position(event: &VEvent, date: NaiveDate) -> (f32, f32, bool)
(start_pixels, duration_pixels, false) // is_all_day = false
}
// Check if two events overlap in time
fn events_overlap(event1: &VEvent, event2: &VEvent) -> bool {
let start1 = event1.dtstart.with_timezone(&Local).naive_local();
let end1 = if let Some(end) = event1.dtend {
end.with_timezone(&Local).naive_local()
} else {
start1 + chrono::Duration::hours(1) // Default 1 hour duration
};
let start2 = event2.dtstart.with_timezone(&Local).naive_local();
let end2 = if let Some(end) = event2.dtend {
end.with_timezone(&Local).naive_local()
} else {
start2 + chrono::Duration::hours(1) // Default 1 hour duration
};
// Events overlap if one starts before the other ends
start1 < end2 && start2 < end1
}
// Calculate layout columns for overlapping events
fn calculate_event_layout(events: &[VEvent], date: NaiveDate) -> Vec<(usize, usize)> {
// Filter events that should appear on this date and sort by start time
let mut day_events: Vec<_> = events.iter()
.filter(|event| {
let (_, _, _) = calculate_event_position(event, date);
// Only include events that would be positioned (non-zero dimensions or all-day)
let local_start = event.dtstart.with_timezone(&Local);
let event_date = local_start.date_naive();
event_date == date ||
(event_date == date - chrono::Duration::days(1) && local_start.hour() >= 20)
})
.collect();
// Sort by start time
day_events.sort_by_key(|event| event.dtstart.with_timezone(&Local).naive_local());
// Calculate layout: (column_index, total_columns)
let mut layout = Vec::with_capacity(day_events.len());
let mut columns: Vec<Vec<&VEvent>> = Vec::new();
for event in &day_events {
// Find the first column where this event doesn't overlap with any existing event
let mut placed = false;
for (col_idx, column) in columns.iter_mut().enumerate() {
if !column.iter().any(|existing_event| events_overlap(event, existing_event)) {
column.push(event);
layout.push((col_idx, 0)); // total_columns will be set later
placed = true;
break;
}
}
if !placed {
// Create new column
columns.push(vec![event]);
layout.push((columns.len() - 1, 0)); // total_columns will be set later
}
}
// Update total_columns for all events
let total_columns = columns.len();
for (_, total_cols) in layout.iter_mut() {
*total_cols = total_columns;
}
// Create result mapping original events to their layout
let mut result = Vec::with_capacity(events.len());
for event in events {
if let Some(pos) = day_events.iter().position(|e| e.uid == event.uid) {
result.push(layout[pos]);
} else {
result.push((0, 1)); // Default: single column
}
}
result
}

View File

@@ -782,8 +782,7 @@ body {
/* Week Events */
.week-event {
position: absolute !important;
left: 4px;
right: 4px;
/* left and width are now set inline for overlap handling */
min-height: 20px;
background: #3B82F6;
color: white;