From e3c3b341e5d858026465d25d0d7adfd45dfcb375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Wed, 25 Mar 2026 15:49:14 +0100 Subject: [PATCH] Calendar improvements cycle 2 --- .../CalendarPageViewModel.cs | 40 ++++--- Wino.Core.Tests/CalendarPageViewModelTests.cs | 108 +++++++++++++++--- Wino.Mail.WinUI/AppThemes/Acrylic.xaml | 2 + Wino.Mail.WinUI/AppThemes/Clouds.xaml | 2 + Wino.Mail.WinUI/AppThemes/Custom.xaml | 2 + Wino.Mail.WinUI/AppThemes/Default.xaml | 2 + Wino.Mail.WinUI/AppThemes/Forest.xaml | 2 + Wino.Mail.WinUI/AppThemes/Garden.xaml | 2 + Wino.Mail.WinUI/AppThemes/Nighty.xaml | 2 + Wino.Mail.WinUI/AppThemes/Snowflake.xaml | 2 + .../CalendarEmptySlotTappedEventArgs.cs | 6 +- .../Calendar/CalendarPeriodControl.xaml | 16 +-- .../Calendar/CalendarPeriodControl.xaml.cs | 96 +++++++++++++++- .../Controls/Calendar/MonthCalendarLayout.cs | 19 ++- .../Views/Calendar/CalendarPage.xaml | 12 +- .../Views/Calendar/CalendarPage.xaml.cs | 81 ++++++++++++- 16 files changed, 332 insertions(+), 62 deletions(-) diff --git a/Wino.Calendar.ViewModels/CalendarPageViewModel.cs b/Wino.Calendar.ViewModels/CalendarPageViewModel.cs index bae67b53..fedfce72 100644 --- a/Wino.Calendar.ViewModels/CalendarPageViewModel.cs +++ b/Wino.Calendar.ViewModels/CalendarPageViewModel.cs @@ -37,9 +37,6 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, { #region Quick Event Creation - [ObservableProperty] - public partial bool IsQuickEventDialogOpen { get; set; } - [ObservableProperty] [NotifyPropertyChangedFor(nameof(SelectedQuickEventAccountCalendarName))] [NotifyCanExecuteChangedFor(nameof(SaveQuickEventCommand))] @@ -252,10 +249,16 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, } private void AccountCalendarStateCollectivelyChanged(object sender, GroupedAccountCalendarViewModel e) - => _ = ReloadCurrentVisibleRangeAsync(); + { + EnsureSelectedQuickEventAccountCalendar(); + _ = ReloadCurrentVisibleRangeAsync(); + } private void UpdateAccountCalendarRequested(object sender, AccountCalendarViewModel e) - => _ = ReloadCurrentVisibleRangeAsync(); + { + EnsureSelectedQuickEventAccountCalendar(); + _ = ReloadCurrentVisibleRangeAsync(); + } [RelayCommand(CanExecute = nameof(CanJoinOnline))] private async Task JoinOnlineAsync() @@ -273,9 +276,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, AttachSubscriptions(); RefreshSettings(); IsCalendarEnabled = true; - - SelectedQuickEventAccountCalendar = AccountCalendarStateService.ActiveCalendars.FirstOrDefault(a => a.IsPrimary) - ?? AccountCalendarStateService.ActiveCalendars.FirstOrDefault(); + EnsureSelectedQuickEventAccountCalendar(); } public override void OnNavigatedFrom(NavigationMode mode, object parameters) @@ -318,7 +319,6 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, DisplayDetailsCalendarItemViewModel = null; SelectedQuickEventAccountCalendar = null; SelectedQuickEventDate = null; - IsQuickEventDialogOpen = false; HourSelectionStrings = []; CurrentVisibleRange = null; VisibleDateRangeText = string.Empty; @@ -469,8 +469,6 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, RecurrenceSummary = string.Empty }; - IsQuickEventDialogOpen = false; - var preparationRequest = new CalendarOperationPreparationRequest( CalendarSynchronizerOperation.CreateEvent, ComposeResult: composeResult); @@ -507,8 +505,6 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, endDate = SelectedQuickEventDate.Value.Date.AddDays(1); } - IsQuickEventDialogOpen = false; - _navigationService.Navigate(WinoPage.CalendarEventComposePage, new CalendarEventComposeNavigationArgs { SelectedCalendarId = SelectedQuickEventAccountCalendar?.Id, @@ -602,6 +598,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, CalendarItems = loadedItems; } + EnsureSelectedQuickEventAccountCalendar(); CurrentVisibleRange = visibleRange; LoadedDateWindow = loadedDateWindow; VisibleDateRangeText = _calendarRangeTextFormatter.Format(visibleRange, _dateContextProvider); @@ -660,7 +657,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, var loadedItems = new Dictionary(); var loadPeriod = new TimeRange(loadedDateWindow.StartDate, loadedDateWindow.EndDate); - foreach (var calendarViewModel in AccountCalendarStateService.AllCalendars) + foreach (var calendarViewModel in AccountCalendarStateService.ActiveCalendars) { if (!IsPageActive(lifetimeVersion)) return []; @@ -711,6 +708,17 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, private bool IsCalendarActive(Guid? calendarId) => calendarId.HasValue && AccountCalendarStateService.ActiveCalendars.Any(calendar => calendar.Id == calendarId.Value); + private void EnsureSelectedQuickEventAccountCalendar() + { + if (SelectedQuickEventAccountCalendar != null && IsCalendarActive(SelectedQuickEventAccountCalendar.Id)) + { + return; + } + + SelectedQuickEventAccountCalendar = AccountCalendarStateService.ActiveCalendars.FirstOrDefault(a => a.IsPrimary) + ?? AccountCalendarStateService.ActiveCalendars.FirstOrDefault(); + } + public async void Receive(LoadCalendarMessage message) => await ApplyDisplayRequestAsync(message.DisplayRequest, message.ForceReload); @@ -742,9 +750,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, DisplayDetailsCalendarItemViewModel = null; } - SelectedQuickEventAccountCalendar = AccountCalendarStateService.ActiveCalendars.FirstOrDefault(a => a.IsPrimary) - ?? AccountCalendarStateService.ActiveCalendars.FirstOrDefault(); - + EnsureSelectedQuickEventAccountCalendar(); await ReloadCurrentVisibleRangeAsync().ConfigureAwait(false); } diff --git a/Wino.Core.Tests/CalendarPageViewModelTests.cs b/Wino.Core.Tests/CalendarPageViewModelTests.cs index f656c137..95239be5 100644 --- a/Wino.Core.Tests/CalendarPageViewModelTests.cs +++ b/Wino.Core.Tests/CalendarPageViewModelTests.cs @@ -94,6 +94,67 @@ public class CalendarPageViewModelTests calendarService.Verify(service => service.GetCalendarEventsAsync(It.IsAny(), It.IsAny()), Times.Exactly(2)); } + [Fact] + public async Task ApplyDisplayRequestAsync_LoadsOnlyActiveCalendars() + { + var settings = CreateSettings(); + var preferencesService = CreatePreferencesService(settings); + var calendarService = new Mock(); + + var account = new MailAccount + { + Id = Guid.NewGuid(), + Name = "Primary", + SenderName = "Primary", + Address = "primary@example.com", + ProviderType = MailProviderType.Outlook + }; + + var visibleCalendar = CreateCalendar(account, "Visible calendar"); + var hiddenCalendar = CreateCalendar(account, "Hidden calendar"); + var visibleCalendarViewModel = new AccountCalendarViewModel(account, visibleCalendar); + var hiddenCalendarViewModel = new AccountCalendarViewModel(account, hiddenCalendar); + hiddenCalendarViewModel.IsChecked = false; + + calendarService + .Setup(service => service.GetCalendarEventsAsync(It.Is(calendar => calendar.Id == visibleCalendar.Id), It.IsAny())) + .ReturnsAsync([ + new CalendarItem + { + Id = Guid.NewGuid(), + CalendarId = visibleCalendar.Id, + StartDate = new DateTime(2026, 3, 20, 9, 0, 0), + DurationInSeconds = TimeSpan.FromMinutes(30).TotalSeconds, + Title = "Visible event" + } + ]); + + calendarService + .Setup(service => service.GetCalendarEventsAsync(It.Is(calendar => calendar.Id == hiddenCalendar.Id), It.IsAny())) + .ReturnsAsync([ + new CalendarItem + { + Id = Guid.NewGuid(), + CalendarId = hiddenCalendar.Id, + StartDate = new DateTime(2026, 3, 20, 10, 0, 0), + DurationInSeconds = TimeSpan.FromMinutes(30).TotalSeconds, + Title = "Hidden event" + } + ]); + + var accountCalendarStateService = new FakeAccountCalendarStateService( + [visibleCalendarViewModel, hiddenCalendarViewModel], + [visibleCalendarViewModel]); + + var viewModel = CreateViewModel(calendarService.Object, preferencesService.Object, new DateOnly(2026, 3, 20), accountCalendarStateService); + + await viewModel.ApplyDisplayRequestAsync(new CalendarDisplayRequest(CalendarDisplayType.Day, new DateOnly(2026, 3, 20))); + + viewModel.CalendarItems.Should().ContainSingle(item => item.CalendarItem.CalendarId == visibleCalendar.Id); + calendarService.Verify(service => service.GetCalendarEventsAsync(It.Is(calendar => calendar.Id == visibleCalendar.Id), It.IsAny()), Times.Once); + calendarService.Verify(service => service.GetCalendarEventsAsync(It.Is(calendar => calendar.Id == hiddenCalendar.Id), It.IsAny()), Times.Never); + } + private static CalendarPageViewModel CreateViewModel( ICalendarService calendarService, IPreferencesService preferencesService, @@ -108,24 +169,19 @@ public class CalendarPageViewModelTests ProviderType = MailProviderType.Outlook }; - var calendar = new AccountCalendar - { - Id = Guid.NewGuid(), - AccountId = account.Id, - Name = "Calendar", - RemoteCalendarId = "calendar", - SynchronizationDeltaToken = string.Empty, - TextColorHex = "#000000", - BackgroundColorHex = "#ffffff", - TimeZone = TimeZoneInfo.Utc.Id, - IsExtended = true, - IsPrimary = true, - IsSynchronizationEnabled = true - }; - + var calendar = CreateCalendar(account, "Calendar"); var accountCalendarViewModel = new AccountCalendarViewModel(account, calendar); var accountCalendarStateService = new FakeAccountCalendarStateService([accountCalendarViewModel]); + return CreateViewModel(calendarService, preferencesService, today, accountCalendarStateService); + } + + private static CalendarPageViewModel CreateViewModel( + ICalendarService calendarService, + IPreferencesService preferencesService, + DateOnly today, + IAccountCalendarStateService accountCalendarStateService) + { var statePersistenceService = new Mock(); statePersistenceService.SetupAllProperties(); statePersistenceService.Object.ApplicationMode = WinoApplicationMode.Calendar; @@ -145,6 +201,22 @@ public class CalendarPageViewModelTests new CalendarRangeTextFormatter()); } + private static AccountCalendar CreateCalendar(MailAccount account, string name) + => new() + { + Id = Guid.NewGuid(), + AccountId = account.Id, + Name = name, + RemoteCalendarId = "calendar", + SynchronizationDeltaToken = string.Empty, + TextColorHex = "#000000", + BackgroundColorHex = "#ffffff", + TimeZone = TimeZoneInfo.Utc.Id, + IsExtended = true, + IsPrimary = true, + IsSynchronizationEnabled = true + }; + private static Mock CreatePreferencesService(CalendarSettings settings) => CreatePreferencesService(() => settings); @@ -177,11 +249,13 @@ public class CalendarPageViewModelTests private sealed class FakeAccountCalendarStateService : IAccountCalendarStateService { private readonly List _calendars; + private readonly List _activeCalendars; private readonly ObservableCollection _groupedCalendars = []; - public FakeAccountCalendarStateService(IEnumerable calendars) + public FakeAccountCalendarStateService(IEnumerable calendars, IEnumerable? activeCalendars = null) { _calendars = calendars.ToList(); + _activeCalendars = (activeCalendars ?? _calendars.Where(calendar => calendar.IsChecked)).ToList(); GroupedAccountCalendars = new ReadOnlyObservableCollection(_groupedCalendars); } @@ -206,7 +280,7 @@ public class CalendarPageViewModelTests remove { } } - public IEnumerable ActiveCalendars => _calendars; + public IEnumerable ActiveCalendars => _activeCalendars; public IEnumerable AllCalendars => _calendars; public ReadOnlyObservableGroupedCollection GroupedCalendars { get; set; } = null!; diff --git a/Wino.Mail.WinUI/AppThemes/Acrylic.xaml b/Wino.Mail.WinUI/AppThemes/Acrylic.xaml index 67386db8..0d26ea38 100644 --- a/Wino.Mail.WinUI/AppThemes/Acrylic.xaml +++ b/Wino.Mail.WinUI/AppThemes/Acrylic.xaml @@ -15,6 +15,7 @@ #ecf0f1 #B2FCFCFC #D9ECEFF1 + #4D0078D4 #2C2C2C #662C2C2C #992C2C2C + #66399BFF #b2dffc #33B2DFFC #66B2DFFC + #4D0078D4 #222f3e @@ -21,6 +22,7 @@ #b2dffc #33B2DFFC #66B2DFFC + #66399BFF #222f3e diff --git a/Wino.Mail.WinUI/AppThemes/Custom.xaml b/Wino.Mail.WinUI/AppThemes/Custom.xaml index 280ea4c9..fc56711b 100644 --- a/Wino.Mail.WinUI/AppThemes/Custom.xaml +++ b/Wino.Mail.WinUI/AppThemes/Custom.xaml @@ -24,6 +24,7 @@ #D9FFFFFF + #4D0078D4 @@ -37,6 +38,7 @@ #E61F1F1F + #66399BFF diff --git a/Wino.Mail.WinUI/AppThemes/Default.xaml b/Wino.Mail.WinUI/AppThemes/Default.xaml index cc21924a..0a1c448e 100644 --- a/Wino.Mail.WinUI/AppThemes/Default.xaml +++ b/Wino.Mail.WinUI/AppThemes/Default.xaml @@ -15,11 +15,13 @@ #ecf0f1 #F7F9FA #DFE4EA + #4D0078D4 #1f1f1f #1F1F1F #262626 + #66399BFF diff --git a/Wino.Mail.WinUI/AppThemes/Forest.xaml b/Wino.Mail.WinUI/AppThemes/Forest.xaml index aa5f2556..83288e03 100644 --- a/Wino.Mail.WinUI/AppThemes/Forest.xaml +++ b/Wino.Mail.WinUI/AppThemes/Forest.xaml @@ -14,11 +14,13 @@ #A800D608 #2200D608 #4D00D608 + #4D0078D4 #59001C01 #22001C01 #59001C01 + #66399BFF diff --git a/Wino.Mail.WinUI/AppThemes/Garden.xaml b/Wino.Mail.WinUI/AppThemes/Garden.xaml index eaa17333..85b3adbe 100644 --- a/Wino.Mail.WinUI/AppThemes/Garden.xaml +++ b/Wino.Mail.WinUI/AppThemes/Garden.xaml @@ -14,6 +14,7 @@ #dcfad8 #26DCFAD8 #59DCFAD8 + #4D0078D4 #576574 @@ -22,6 +23,7 @@ #dcfad8 #26576574 #59576574 + #66399BFF diff --git a/Wino.Mail.WinUI/AppThemes/Nighty.xaml b/Wino.Mail.WinUI/AppThemes/Nighty.xaml index 933ec6a6..5bfcfaa3 100644 --- a/Wino.Mail.WinUI/AppThemes/Nighty.xaml +++ b/Wino.Mail.WinUI/AppThemes/Nighty.xaml @@ -15,12 +15,14 @@ #fdcb6e #33FDCB6E #66FDCB6E + #4D0078D4 #5413191F #2213191F #5413191F + #66399BFF diff --git a/Wino.Mail.WinUI/AppThemes/Snowflake.xaml b/Wino.Mail.WinUI/AppThemes/Snowflake.xaml index 04d1e076..b768b91e 100644 --- a/Wino.Mail.WinUI/AppThemes/Snowflake.xaml +++ b/Wino.Mail.WinUI/AppThemes/Snowflake.xaml @@ -15,12 +15,14 @@ #b0c6dd #33B0C6DD #66B0C6DD + #4D0078D4 #b0c6dd #33B0C6DD #66B0C6DD + #66399BFF diff --git a/Wino.Mail.WinUI/Controls/Calendar/CalendarEmptySlotTappedEventArgs.cs b/Wino.Mail.WinUI/Controls/Calendar/CalendarEmptySlotTappedEventArgs.cs index a1e3fffb..19017c9e 100644 --- a/Wino.Mail.WinUI/Controls/Calendar/CalendarEmptySlotTappedEventArgs.cs +++ b/Wino.Mail.WinUI/Controls/Calendar/CalendarEmptySlotTappedEventArgs.cs @@ -6,14 +6,14 @@ namespace Wino.Calendar.Controls; public sealed class CalendarEmptySlotTappedEventArgs : EventArgs { - public CalendarEmptySlotTappedEventArgs(DateTime clickedDate, Point positionerPoint, Size cellSize) + public CalendarEmptySlotTappedEventArgs(DateTime clickedDate, Point anchorPoint, Size cellSize) { ClickedDate = clickedDate; - PositionerPoint = positionerPoint; + AnchorPoint = anchorPoint; CellSize = cellSize; } public DateTime ClickedDate { get; } - public Point PositionerPoint { get; } + public Point AnchorPoint { get; } public Size CellSize { get; } } diff --git a/Wino.Mail.WinUI/Controls/Calendar/CalendarPeriodControl.xaml b/Wino.Mail.WinUI/Controls/Calendar/CalendarPeriodControl.xaml index fb5c5001..3b9134f8 100644 --- a/Wino.Mail.WinUI/Controls/Calendar/CalendarPeriodControl.xaml +++ b/Wino.Mail.WinUI/Controls/Calendar/CalendarPeriodControl.xaml @@ -9,7 +9,6 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:skia="using:SkiaSharp.Views.Windows" xmlns:viewModels="using:Wino.Calendar.ViewModels.Data" - x:Name="Root" SizeChanged="ControlSizeChanged" mc:Ignorable="d"> @@ -22,10 +21,7 @@ - + @@ -77,8 +73,8 @@ x:Name="TimedHeaderHost" Grid.Row="0" Grid.ColumnSpan="2" - Margin="64,0,0,0" Height="44" + Margin="64,0,0,0" Background="{ThemeResource LayerFillColorDefaultBrush}"> - + + + + + + + diff --git a/Wino.Mail.WinUI/Controls/Calendar/CalendarPeriodControl.xaml.cs b/Wino.Mail.WinUI/Controls/Calendar/CalendarPeriodControl.xaml.cs index 53bc7be0..75b45e01 100644 --- a/Wino.Mail.WinUI/Controls/Calendar/CalendarPeriodControl.xaml.cs +++ b/Wino.Mail.WinUI/Controls/Calendar/CalendarPeriodControl.xaml.cs @@ -35,6 +35,7 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty private const double TimedHourColumnWidth = 64d; private const double TimedGridIntervalMinutes = 30d; private const double TimedSelectionIntervalMinutes = 30d; + private const double TimedItemRightSpacing = 10d; private VisibleDateRange _currentRange = new( CalendarDisplayType.Month, DateOnly.FromDateTime(DateTime.Today), @@ -78,6 +79,12 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty [GeneratedDependencyProperty] public partial Brush? WorkHourBackground { get; set; } + [GeneratedDependencyProperty] + public partial Brush? SelectedSlotBackground { get; set; } + + [GeneratedDependencyProperty] + public partial DateTime? SelectedDateTime { get; set; } + public CalendarPeriodControl() { InitializeComponent(); @@ -153,6 +160,8 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty partial void OnVisibleRangeChanged(VisibleDateRange? newValue) => RequestRefresh(); partial void OnCalendarSettingsChanged(CalendarSettings? newValue) => RequestRefresh(); partial void OnTimedHeaderDateFormatChanged(string? newValue) => RequestRefresh(); + partial void OnSelectedSlotBackgroundChanged(Brush? newValue) => InvalidateStructureCanvases(); + partial void OnSelectedDateTimeChanged(DateTime? newValue) => InvalidateStructureCanvases(); partial void OnCalendarItemsChanged(IReadOnlyList? newValue) { @@ -231,6 +240,12 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty QueueRefresh(); } + private void InvalidateStructureCanvases() + { + TimedStructureCanvas.Invalidate(); + MonthStructureCanvas.Invalidate(); + } + private void Refresh() { if (!_refreshPending || !IsLoaded || ActualWidth <= 0 || VisibleRange is null || CalendarSettings is null) @@ -461,6 +476,7 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty using var minorLinePaint = CreateMinorLinePaint(); using var defaultFillPaint = CreateFillPaint(DefaultHourBackground ?? new SolidColorBrush(Colors.Transparent)); using var workFillPaint = CreateFillPaint(WorkHourBackground ?? new SolidColorBrush(Colors.Transparent)); + using var selectedFillPaint = CreateFillPaint(SelectedSlotBackground ?? new SolidColorBrush(Colors.Transparent)); var canvas = e.Surface.Canvas; canvas.Clear(SKColors.Transparent); @@ -499,6 +515,12 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty } } + var selectedTimedSlotRect = GetSelectedTimedSlotRect(dayWidth, intervalHeight, intervalCount); + if (selectedTimedSlotRect.HasValue && selectedFillPaint.Color.Alpha > 0) + { + canvas.DrawRect(selectedTimedSlotRect.Value, selectedFillPaint); + } + for (var intervalIndex = 0; intervalIndex <= intervalCount; intervalIndex++) { var y = intervalIndex * intervalHeight; @@ -522,6 +544,7 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty Color = new SKColor(0, 120, 215, 26), IsAntialias = true }; + using var selectedPaint = CreateFillPaint(SelectedSlotBackground ?? new SolidColorBrush(Colors.Transparent)); var canvas = e.Surface.Canvas; canvas.Clear(SKColors.Transparent); @@ -545,6 +568,12 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty canvas.DrawRect((float)cell.Bounds.X, (float)cell.Bounds.Y, (float)cell.Bounds.Width, (float)cell.Bounds.Height, todayPaint); } + var selectedMonthCellRect = GetSelectedMonthCellRect(); + if (selectedMonthCellRect.HasValue && selectedPaint.Color.Alpha > 0) + { + canvas.DrawRect(selectedMonthCellRect.Value, selectedPaint); + } + for (var row = 0; row <= MonthCalendarLayoutCalculator.RowCount; row++) { var y = row * cellHeight; @@ -604,7 +633,7 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty { var presenter = new ContentPresenter { - Width = item.Bounds.Width, + Width = Math.Max(0d, item.Bounds.Width - TimedItemRightSpacing), Height = item.Bounds.Height, Content = item.Item, ContentTemplate = item.Template @@ -669,12 +698,13 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty var slotIndex = Math.Clamp((int)(position.Y / intervalHeight), 0, (int)((24d * 60d / TimedSelectionIntervalMinutes) - 1)); var slotStart = TimeSpan.FromMinutes(slotIndex * TimedSelectionIntervalMinutes); var clickedDate = _timedLayout.VisibleDates[dayIndex].ToDateTime(TimeOnly.MinValue).Add(slotStart); + var anchorPoint = TimedViewport.TransformToVisual(Root).TransformPoint(position); EmptySlotTapped?.Invoke( this, new CalendarEmptySlotTappedEventArgs( clickedDate, - new Point(dayIndex * _timedLayout.DayWidth, slotIndex * intervalHeight), + anchorPoint, new Size(_timedLayout.DayWidth, intervalHeight))); } @@ -690,15 +720,75 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty var row = Math.Clamp((int)(position.Y / _monthLayout.CellHeight), 0, MonthCalendarLayoutCalculator.RowCount - 1); var cellIndex = Math.Clamp((row * MonthCalendarLayoutCalculator.ColumnCount) + column, 0, _monthLayout.Cells.Count - 1); var cell = _monthLayout.Cells[cellIndex]; + var anchorPoint = MonthViewport.TransformToVisual(Root).TransformPoint(position); EmptySlotTapped?.Invoke( this, new CalendarEmptySlotTappedEventArgs( cell.Date.ToDateTime(TimeOnly.MinValue), - new Point(cell.Bounds.X, cell.Bounds.Y), + anchorPoint, new Size(cell.Bounds.Width, cell.Bounds.Height))); } + private SKRect? GetSelectedTimedSlotRect(float dayWidth, float intervalHeight, int intervalCount) + { + if (SelectedDateTime is not DateTime selectedDateTime || _timedLayout.VisibleDates.Count == 0) + { + return null; + } + + var dayIndex = FindVisibleDateIndex(DateOnly.FromDateTime(selectedDateTime)); + if (dayIndex < 0) + { + return null; + } + + var slotIndex = (int)Math.Floor(selectedDateTime.TimeOfDay.TotalMinutes / TimedSelectionIntervalMinutes); + slotIndex = Math.Clamp(slotIndex, 0, intervalCount - 1); + + var x = dayIndex * dayWidth; + var y = slotIndex * intervalHeight; + return new SKRect(x, y, x + dayWidth, y + intervalHeight); + } + + private SKRect? GetSelectedMonthCellRect() + { + if (SelectedDateTime is not DateTime selectedDateTime) + { + return null; + } + + var selectedDate = DateOnly.FromDateTime(selectedDateTime); + foreach (var cell in _monthLayout.Cells) + { + if (cell.Date != selectedDate) + { + continue; + } + + return new SKRect( + (float)cell.Bounds.X, + (float)cell.Bounds.Y, + (float)(cell.Bounds.X + cell.Bounds.Width), + (float)(cell.Bounds.Y + cell.Bounds.Height)); + } + + return null; + } + + private int FindVisibleDateIndex(DateOnly date) + { + for (var index = 0; index < _timedLayout.VisibleDates.Count; index++) + { + if (_timedLayout.VisibleDates[index] == date) + { + return index; + } + } + + return -1; + } + private double GetTimedSurfaceWidth() => Math.Max(0d, ActualWidth - TimedHourColumnWidth); private string GetTimedHeaderText(DateOnly date) diff --git a/Wino.Mail.WinUI/Controls/Calendar/MonthCalendarLayout.cs b/Wino.Mail.WinUI/Controls/Calendar/MonthCalendarLayout.cs index ff348651..387ddce6 100644 --- a/Wino.Mail.WinUI/Controls/Calendar/MonthCalendarLayout.cs +++ b/Wino.Mail.WinUI/Controls/Calendar/MonthCalendarLayout.cs @@ -66,7 +66,8 @@ internal static class MonthCalendarLayoutCalculator private const double CellPadding = 4d; private const double DayLabelHeight = 20d; - private const double ItemHeight = 18d; + private const double RegularItemHeight = 18d; + private const double ExpandedItemHeight = 30d; private const double ItemGap = 2d; public static MonthCalendarLayoutResult Calculate(VisibleDateRange range, IEnumerable items, double availableWidth, double availableHeight) @@ -92,12 +93,13 @@ internal static class MonthCalendarLayoutCalculator foreach (var cell in cells) { var cellItems = GetCellItems(items, cell.Date).ToList(); + var nextItemY = cell.Bounds.Y + DayLabelHeight + CellPadding; for (var index = 0; index < cellItems.Count; index++) { - var y = cell.Bounds.Y + DayLabelHeight + CellPadding + (index * (ItemHeight + ItemGap)); + var itemHeight = GetItemHeight(cellItems[index]); - if (y + ItemHeight > cell.Bounds.Y + cell.Bounds.Height - CellPadding) + if (nextItemY + itemHeight > cell.Bounds.Y + cell.Bounds.Height - CellPadding) { break; } @@ -108,9 +110,11 @@ internal static class MonthCalendarLayoutCalculator cell.Date, new LayoutRect( cell.Bounds.X + CellPadding, - y, + nextItemY, Math.Max(0, cell.Bounds.Width - (CellPadding * 2)), - ItemHeight))); + itemHeight))); + + nextItemY += itemHeight + ItemGap; } } @@ -143,4 +147,9 @@ internal static class MonthCalendarLayoutCalculator } } } + + private static double GetItemHeight(CalendarItemViewModel item) + => item.IsAllDayEvent || item.IsMultiDayEvent + ? ExpandedItemHeight + : RegularItemHeight; } diff --git a/Wino.Mail.WinUI/Views/Calendar/CalendarPage.xaml b/Wino.Mail.WinUI/Views/Calendar/CalendarPage.xaml index d3ef5db6..cbafe416 100644 --- a/Wino.Mail.WinUI/Views/Calendar/CalendarPage.xaml +++ b/Wino.Mail.WinUI/Views/Calendar/CalendarPage.xaml @@ -44,6 +44,8 @@ DefaultHourBackground="{ThemeResource CalendarDefaultHourBackgroundBrush}" EmptySlotTapped="CalendarSurfaceEmptySlotTapped" IsEnabled="{x:Bind ViewModel.IsCalendarEnabled, Mode=OneWay}" + SelectedDateTime="{x:Bind ViewModel.SelectedQuickEventDate, Mode=OneWay}" + SelectedSlotBackground="{ThemeResource CalendarSelectedHourBackgroundBrush}" VisibleRange="{x:Bind ViewModel.CurrentVisibleRange, Mode=OneWay}" WorkHourBackground="{ThemeResource CalendarWorkHourBackgroundBrush}" /> @@ -64,7 +66,6 @@ Closed="QuickEventPopupClosed" DesiredPlacement="{x:Bind helpers:XamlHelpers.GetPlaccementModeForCalendarType(ViewModel.StatePersistanceService.CalendarDisplayType), Mode=OneWay}" IsLightDismissEnabled="True" - IsOpen="{x:Bind ViewModel.IsQuickEventDialogOpen, Mode=TwoWay}" PlacementTarget="{x:Bind TeachingTipPositionerGrid}"> @@ -226,14 +227,17 @@ - -