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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user