Calendar rendering implementation.

This commit is contained in:
Burak Kaan Köse
2026-03-23 14:56:36 +01:00
parent 8586d0ef54
commit 1adba271e2
32 changed files with 11146 additions and 846 deletions
+5
View File
@@ -15,6 +15,9 @@ Wino Mail is a native Windows mail client (Windows 10 1809+ / Windows 11) replac
# Build WinUI project (Debug x64)
dotnet restore Wino.Mail.WinUI/Wino.Mail.WinUI.csproj --configfile nuget.config -p:Platform=x64 -p:RuntimeIdentifier=win-x64 && dotnet build Wino.Mail.WinUI/Wino.Mail.WinUI.csproj -c Debug --no-restore /p:Platform=x64 /p:RuntimeIdentifier=win-x64 /p:GenerateAppxPackageOnBuild=false /p:AppxPackageSigningEnabled=false
# Build WinUI project with diagnostic XAML/compiler logging (use when plain build only shows "XamlCompiler.exe exited with code 1")
dotnet build Wino.Mail.WinUI/Wino.Mail.WinUI.csproj -c Debug --no-restore /p:Platform=x64 /p:RuntimeIdentifier=win-x64 /p:GenerateAppxPackageOnBuild=false /p:AppxPackageSigningEnabled=false "/flp:logfile=winui-build.log;verbosity=diagnostic" /bl:winui-build.binlog
# Run tests (Debug x64)
dotnet test Wino.Core.Tests/Wino.Core.Tests.csproj -c Debug /p:Platform=x64
@@ -36,6 +39,7 @@ dotnet restore Wino.Mail.WinUI/Wino.Mail.WinUI.csproj --configfile nuget.config
- After the first restore, prefer `--no-restore` builds unless package or project references changed
- Summarize long build logs and inspect only the files named in diagnostics instead of loading large logs into context
- When the prompt already names likely files, types, or symbols, start there instead of re-mapping the repository
- If a WinUI build only reports `XamlCompiler.exe exited with code 1`, rerun with the diagnostic logging command above and inspect the terminal output plus `winui-build.log` for real `WMC`/`WMC1121`/binding diagnostics before guessing
## Architecture
@@ -99,6 +103,7 @@ private string searchQuery = string.Empty;
- **NEVER** create IValueConverter classes
- WinUI 3 auto-converts bool to Visibility: `Visibility="{x:Bind IsVisible, Mode=OneWay}"`
- Use XamlHelpers for complex conversions: `{x:Bind helpers:XamlHelpers.ReverseBoolToVisibilityConverter(Prop)}`
- `x:Bind` does not implicitly convert `double` to `GridLength`; when binding `RowDefinition.Height` or `ColumnDefinition.Width`, use a `XamlHelpers` method such as `DoubleToGridLength(...)`
## Localization
@@ -41,6 +41,12 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel
[ObservableProperty]
public partial int WorkingDayEndIndex { get; set; }
[ObservableProperty]
public partial string TimedDayHeaderDateFormat { get; set; } = "ddd dd";
[ObservableProperty]
public partial int SelectedTimedDayHeaderFormatPresetIndex { get; set; } = -1;
[ObservableProperty]
public partial List<string> ReminderOptions { get; set; } = [];
@@ -56,6 +62,14 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel
public ObservableCollection<MailAccount> Accounts { get; } = [];
public ObservableCollection<CalendarNewEventBehaviorOption> NewEventBehaviorOptions { get; } = [];
public ObservableCollection<AccountCalendarViewModel> AvailableNewEventCalendars { get; } = [];
public ObservableCollection<string> TimedDayHeaderFormatPresets { get; } =
[
"ddd dd",
"dddd dd",
"ddd d MMM",
"dd MMM ddd",
"M/d ddd"
];
[ObservableProperty]
public partial CalendarNewEventBehaviorOption SelectedNewEventBehaviorOption { get; set; }
@@ -68,6 +82,7 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel
public IPreferencesService PreferencesService { get; }
private readonly ICalendarService _calendarService;
private readonly IAccountService _accountService;
private readonly CultureInfo _calendarCulture;
private readonly bool _isLoaded = false;
public CalendarSettingsPageViewModel(IPreferencesService preferencesService, ICalendarService calendarService, IAccountService accountService)
@@ -78,6 +93,7 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel
var currentLanguageLanguageCode = WinoTranslationDictionary.GetLanguageFileNameRelativePath(preferencesService.CurrentLanguage);
var cultureInfo = new CultureInfo(currentLanguageLanguageCode);
_calendarCulture = cultureInfo;
for (var i = 0; i < 7; i++)
{
@@ -92,6 +108,8 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel
CellHourHeight = preferencesService.HourHeight;
WorkingDayStartIndex = DayNames.IndexOf(cultureInfo.DateTimeFormat.GetDayName(preferencesService.WorkingDayStart));
WorkingDayEndIndex = DayNames.IndexOf(cultureInfo.DateTimeFormat.GetDayName(preferencesService.WorkingDayEnd));
TimedDayHeaderDateFormat = preferencesService.CalendarTimedDayHeaderDateFormat;
SelectedTimedDayHeaderFormatPresetIndex = TimedDayHeaderFormatPresets.IndexOf(TimedDayHeaderDateFormat);
var predefinedMinutes = _calendarService.GetPredefinedReminderMinutes();
ReminderOptions.Add("None");
@@ -169,12 +187,52 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel
}
partial void OnCellHourHeightChanged(double oldValue, double newValue) => SaveSettings();
partial void OnIs24HourHeadersChanged(bool value) => SaveSettings();
partial void OnIs24HourHeadersChanged(bool value)
{
OnPropertyChanged(nameof(TimedHourLabelPreview));
SaveSettings();
}
partial void OnSelectedFirstDayOfWeekIndexChanged(int value) => SaveSettings();
partial void OnWorkingHourStartChanged(TimeSpan value) => SaveSettings();
partial void OnWorkingHourEndChanged(TimeSpan value) => SaveSettings();
partial void OnWorkingDayStartIndexChanged(int value) => SaveSettings();
partial void OnWorkingDayEndIndexChanged(int value) => SaveSettings();
partial void OnTimedDayHeaderDateFormatChanged(string value)
{
OnPropertyChanged(nameof(TimedDayHeaderFormatPreview));
OnPropertyChanged(nameof(TimedHourLabelPreview));
var normalizedFormat = string.IsNullOrWhiteSpace(value) ? "ddd dd" : value.Trim();
var matchingPresetIndex = TimedDayHeaderFormatPresets
.Select((format, index) => new { format, index })
.Where(item => string.Equals(item.format, normalizedFormat, StringComparison.Ordinal))
.Select(item => item.index)
.DefaultIfEmpty(-1)
.First();
if (SelectedTimedDayHeaderFormatPresetIndex != matchingPresetIndex)
{
SelectedTimedDayHeaderFormatPresetIndex = matchingPresetIndex;
}
SaveSettings();
}
partial void OnSelectedTimedDayHeaderFormatPresetIndexChanged(int value)
{
if (value < 0 || value >= TimedDayHeaderFormatPresets.Count)
{
return;
}
var selectedPreset = TimedDayHeaderFormatPresets[value];
if (string.Equals(TimedDayHeaderDateFormat, selectedPreset, StringComparison.Ordinal))
{
return;
}
TimedDayHeaderDateFormat = selectedPreset;
}
partial void OnSelectedDefaultReminderIndexChanged(int value) => SaveSettings();
partial void OnSelectedDefaultSnoozeIndexChanged(int value) => SaveSettings();
partial void OnSelectedNewEventBehaviorOptionChanged(CalendarNewEventBehaviorOption value)
@@ -184,6 +242,49 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel
}
partial void OnSelectedNewEventCalendarChanged(AccountCalendarViewModel value) => SaveSettings();
public string TimedDayHeaderFormatPreview
{
get
{
var format = string.IsNullOrWhiteSpace(TimedDayHeaderDateFormat) ? "ddd dd" : TimedDayHeaderDateFormat.Trim();
var previewDates = new[]
{
new DateTime(2026, 3, 23),
new DateTime(2026, 3, 24),
new DateTime(2026, 3, 25)
};
try
{
return string.Join(" · ", previewDates.Select(date => date.ToString(format, _calendarCulture)));
}
catch (FormatException)
{
return string.Join(" · ", previewDates.Select(date => date.ToString("ddd dd", _calendarCulture)));
}
}
}
public string TimedHourLabelPreview
{
get
{
var previewHours = new[] { 0, 9, 14, 24 };
return string.Join(" · ", previewHours.Select(CurrentSettingsPreviewLabel));
}
}
private string CurrentSettingsPreviewLabel(int hour)
{
if (Is24HourHeaders)
{
return hour.ToString(_calendarCulture);
}
var displayHour = hour % 24;
return DateTime.Today.AddHours(displayHour).ToString("h tt", _calendarCulture);
}
public void SaveSettings()
{
if (!_isLoaded)
@@ -229,6 +330,7 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel
PreferencesService.WorkingHourStart = WorkingHourStart;
PreferencesService.WorkingHourEnd = WorkingHourEnd;
PreferencesService.HourHeight = CellHourHeight;
PreferencesService.CalendarTimedDayHeaderDateFormat = TimedDayHeaderDateFormat;
if (SelectedDefaultReminderIndex == 0)
{
@@ -1,16 +1,12 @@
using Wino.Calendar.ViewModels.Data;
using Wino.Core.Domain.Models.Calendar;
namespace Wino.Calendar.ViewModels.Messages;
public class CalendarItemTappedMessage
{
public CalendarItemTappedMessage(CalendarItemViewModel calendarItemViewModel, CalendarDayModel clickedPeriod)
public CalendarItemTappedMessage(CalendarItemViewModel calendarItemViewModel)
{
CalendarItemViewModel = calendarItemViewModel;
ClickedPeriod = clickedPeriod;
}
public CalendarItemViewModel CalendarItemViewModel { get; }
public CalendarDayModel ClickedPeriod { get; }
}
@@ -1,41 +0,0 @@
using System.Linq;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Calendar;
namespace Wino.Core.Domain.Collections;
public class DayRangeCollection : ObservableRangeCollection<DayRangeRenderModel>
{
/// <summary>
/// Gets the range of dates that are currently displayed in the collection.
/// </summary>
public DateRange DisplayRange
{
get
{
if (Count == 0) return null;
var minimumLoadedDate = this[0].CalendarRenderOptions.DateRange.StartDate;
var maximumLoadedDate = this[Count - 1].CalendarRenderOptions.DateRange.EndDate;
return new DateRange(minimumLoadedDate, maximumLoadedDate);
}
}
public void RemoveCalendarItem(ICalendarItem calendarItem)
{
foreach (var dayRange in this)
{
}
}
public void AddCalendarItem(ICalendarItem calendarItem)
{
foreach (var dayRange in this)
{
var calendarDayModel = dayRange.CalendarDays.FirstOrDefault(x => x.Period.HasInside(calendarItem.Period.Start));
calendarDayModel?.EventsCollection.AddCalendarItem(calendarItem);
}
}
}
@@ -236,6 +236,7 @@ public interface IPreferencesService : INotifyPropertyChanged
DayOfWeek WorkingDayStart { get; set; }
DayOfWeek WorkingDayEnd { get; set; }
double HourHeight { get; set; }
string CalendarTimedDayHeaderDateFormat { get; set; }
/// <summary>
/// Setting: Default reminder duration in seconds for new calendar events.
@@ -1,26 +0,0 @@
using System;
using Itenso.TimePeriod;
using Wino.Core.Domain.Collections;
namespace Wino.Core.Domain.Models.Calendar;
/// <summary>
/// Represents a day in the calendar.
/// Can hold events, appointments, wheather status etc.
/// </summary>
public class CalendarDayModel
{
public ITimePeriod Period { get; }
public CalendarEventCollection EventsCollection { get; }
public CalendarDayModel(DateTime representingDate, CalendarRenderOptions calendarRenderOptions)
{
RepresentingDate = representingDate;
Period = new TimeRange(representingDate, representingDate.AddDays(1));
CalendarRenderOptions = calendarRenderOptions;
EventsCollection = new CalendarEventCollection(Period, calendarRenderOptions.CalendarSettings);
}
public DateTime RepresentingDate { get; }
public CalendarRenderOptions CalendarRenderOptions { get; }
}
@@ -1,13 +0,0 @@
namespace Wino.Core.Domain.Models.Calendar;
public class CalendarRenderOptions
{
public CalendarRenderOptions(DateRange dateRange, CalendarSettings calendarSettings)
{
DateRange = dateRange;
CalendarSettings = calendarSettings;
}
public int TotalDayCount => DateRange.TotalDays;
public DateRange DateRange { get; }
public CalendarSettings CalendarSettings { get; }
}
@@ -13,7 +13,8 @@ public record CalendarSettings(DayOfWeek FirstDayOfWeek,
TimeSpan WorkingHourEnd,
double HourHeight,
DayHeaderDisplayType DayHeaderDisplayType,
CultureInfo CultureInfo)
CultureInfo CultureInfo,
string TimedDayHeaderDateFormat = "ddd dd")
{
public int WorkWeekDayCount
{
@@ -65,4 +66,35 @@ public record CalendarSettings(DayOfWeek FirstDayOfWeek,
var dateTime = DateTime.Today.Add(timeSpan);
return dateTime.ToString(format, CultureInfo.InvariantCulture);
}
public string GetTimedDayHeaderText(DateOnly date)
{
var format = string.IsNullOrWhiteSpace(TimedDayHeaderDateFormat) ? "ddd dd" : TimedDayHeaderDateFormat;
try
{
return date.ToDateTime(TimeOnly.MinValue).ToString(format, CultureInfo);
}
catch (FormatException)
{
return date.ToDateTime(TimeOnly.MinValue).ToString("ddd dd", CultureInfo);
}
}
public string GetTimedHourLabelText(int hour)
{
if (hour < 0 || hour > 24)
{
throw new ArgumentOutOfRangeException(nameof(hour));
}
if (DayHeaderDisplayType == DayHeaderDisplayType.TwentyFourHour)
{
return hour.ToString(CultureInfo);
}
var displayHour = hour % 24;
var dateTime = DateTime.Today.AddHours(displayHour);
return dateTime.ToString("h tt", CultureInfo);
}
}
@@ -1,13 +0,0 @@
namespace Wino.Core.Domain.Models.Calendar;
public class DayHeaderRenderModel
{
public DayHeaderRenderModel(string dayHeader, double hourHeight)
{
DayHeader = dayHeader;
HourHeight = hourHeight;
}
public string DayHeader { get; }
public double HourHeight { get; }
}
@@ -1,53 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Itenso.TimePeriod;
using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Models.Calendar;
/// <summary>
/// Represents a range of days in the calendar.
/// Corresponds to 1 view of the FlipView in CalendarPage.
/// </summary>
public class DayRangeRenderModel
{
public ITimePeriod Period { get; }
public List<CalendarDayModel> CalendarDays { get; } = [];
// TODO: Get rid of this at some point.
public List<DayHeaderRenderModel> DayHeaders { get; } = [];
public CalendarRenderOptions CalendarRenderOptions { get; }
public int TotalDays => CalendarRenderOptions.TotalDayCount;
public DayRangeRenderModel(CalendarRenderOptions calendarRenderOptions)
{
CalendarRenderOptions = calendarRenderOptions;
for (var i = 0; i < CalendarRenderOptions.TotalDayCount; i++)
{
var representingDate = calendarRenderOptions.DateRange.StartDate.AddDays(i);
var calendarDayModel = new CalendarDayModel(representingDate, calendarRenderOptions);
CalendarDays.Add(calendarDayModel);
}
Period = new TimeRange(CalendarDays.First().RepresentingDate, CalendarDays.Last().RepresentingDate.AddDays(1));
// Create day headers based on culture info.
for (var i = 0; i < 24; i++)
{
var representingDate = calendarRenderOptions.DateRange.StartDate.Date.AddHours(i);
string dayHeader = calendarRenderOptions.CalendarSettings.DayHeaderDisplayType switch
{
DayHeaderDisplayType.TwelveHour => representingDate.ToString("h tt", calendarRenderOptions.CalendarSettings.CultureInfo),
DayHeaderDisplayType.TwentyFourHour => representingDate.ToString("HH", calendarRenderOptions.CalendarSettings.CultureInfo),
_ => "N/A"
};
DayHeaders.Add(new DayHeaderRenderModel(dayHeader, calendarRenderOptions.CalendarSettings.HourHeight));
}
}
}
@@ -731,6 +731,8 @@
"SettingsCalendarSettings_Title": "Calendar Settings",
"CalendarSettings_DefaultSnoozeDuration_Header": "Default snooze duration",
"CalendarSettings_DefaultSnoozeDuration_Description": "Set a default snooze duration for calendar reminder notifications.",
"CalendarSettings_TimedDayHeaderFormat_Header": "Timed view day header format",
"CalendarSettings_TimedDayHeaderFormat_Description": "Choose how the top day labels are rendered in day, week, and work week views. Use date format tokens like ddd, dd, MMM, or dddd.",
"SettingsComposer_Title": "Composer",
"SettingsComposerFont_Title": "Default Composer Font",
"SettingsComposerFontFamily_Description": "Change the default font family and font size for composing mails.",
@@ -0,0 +1,19 @@
using System;
using Microsoft.UI.Xaml;
using Windows.Foundation;
namespace Wino.Calendar.Controls;
public sealed class CalendarEmptySlotTappedEventArgs : EventArgs
{
public CalendarEmptySlotTappedEventArgs(DateTime clickedDate, Point positionerPoint, Size cellSize)
{
ClickedDate = clickedDate;
PositionerPoint = positionerPoint;
CellSize = cellSize;
}
public DateTime ClickedDate { get; }
public Point PositionerPoint { get; }
public Size CellSize { get; }
}
@@ -9,8 +9,6 @@ using Microsoft.UI.Xaml.Media;
using Wino.Calendar.ViewModels.Data;
using Wino.Calendar.ViewModels.Messages;
using Wino.Core.Domain;
using Wino.Core.Domain.Models.Calendar;
namespace Wino.Calendar.Controls;
public sealed partial class CalendarItemControl : UserControl
@@ -96,7 +94,7 @@ public sealed partial class CalendarItemControl : UserControl
if (isSingleTap && CalendarItem != null)
{
WeakReferenceMessenger.Default.Send(new CalendarItemTappedMessage(CalendarItem, null));
WeakReferenceMessenger.Default.Send(new CalendarItemTappedMessage(CalendarItem));
}
}
@@ -4,6 +4,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="using:Wino.Helpers"
xmlns:local="using:Wino.Calendar.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:skia="using:SkiaSharp.Views.Windows"
@@ -53,14 +54,34 @@
<Grid>
<Grid x:Name="TimedRoot" Visibility="Collapsed">
<Grid.RowDefinitions>
<RowDefinition Height="44" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="64" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid x:Name="TimedHeaderHost">
<Border
x:Name="TimedHourHeaderHost"
Grid.Row="0"
Grid.Column="0"
Height="44"
Background="{ThemeResource LayerFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="0,0,1,1" />
<Grid
x:Name="TimedHeaderHost"
Grid.Row="0"
Grid.ColumnSpan="2"
Margin="64,0,0,0"
Height="44"
Background="{ThemeResource LayerFillColorDefaultBrush}">
<skia:SKXamlCanvas x:Name="TimedHeaderCanvas" PaintSurface="TimedHeaderCanvasPaintSurface" />
<ItemsControl
x:Name="TimedHeadersItemsControl"
HorizontalContentAlignment="Stretch"
ItemTemplate="{StaticResource TimedHeaderTemplate}"
ItemsSource="{x:Bind TimedHeaderTexts, Mode=OneWay}">
<ItemsControl.ItemsPanel>
@@ -69,19 +90,44 @@
</ItemsControl>
</Grid>
<Grid
Grid.Row="1"
Grid.ColumnSpan="2"
Background="Transparent">
<ScrollViewer
x:Name="TimedScrollViewer"
Grid.Row="1"
Background="Transparent"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto"
VerticalScrollMode="Enabled">
<Grid x:Name="TimedViewport" Height="{x:Bind TimelineHeight, Mode=OneWay}">
<Grid x:Name="TimedScrollContentGrid" Height="{x:Bind TimelineHeight, Mode=OneWay}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="64" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Canvas
x:Name="HourLabelsCanvas"
Width="64"
IsHitTestVisible="False" />
<Grid
x:Name="TimedViewport"
Grid.Column="1"
Height="{x:Bind TimelineHeight, Mode=OneWay}">
<skia:SKXamlCanvas x:Name="TimedStructureCanvas" PaintSurface="TimedStructureCanvasPaintSurface" />
<Border
x:Name="TimedInteractionLayer"
Background="Transparent"
Tapped="TimedInteractionLayerTapped" />
<Canvas x:Name="TimedItemsCanvas" />
</Grid>
</Grid>
</ScrollViewer>
</Grid>
</Grid>
<Grid x:Name="MonthRoot" Visibility="Collapsed">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
@@ -106,7 +152,11 @@
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<skia:SKXamlCanvas x:Name="MonthStructureCanvas" PaintSurface="MonthStructureCanvasPaintSurface" />
<Canvas x:Name="MonthCellLabelsCanvas" />
<Border
x:Name="MonthInteractionLayer"
Background="Transparent"
Tapped="MonthInteractionLayerTapped" />
<Canvas x:Name="MonthCellLabelsCanvas" IsHitTestVisible="False" />
<Canvas x:Name="MonthItemsCanvas" />
</Grid>
</Grid>
@@ -12,9 +12,11 @@ using Microsoft.UI.Composition;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Hosting;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using SkiaSharp;
using SkiaSharp.Views.Windows;
using Windows.Foundation;
using Windows.UI;
using Wino.Calendar.ViewModels.Data;
using Wino.Core.Domain.Enums;
@@ -25,6 +27,9 @@ namespace Wino.Calendar.Controls;
public sealed partial class CalendarPeriodControl : UserControl, INotifyPropertyChanged
{
private const double TimedHourColumnWidth = 64d;
private const double TimedGridIntervalMinutes = 30d;
private const double TimedSelectionIntervalMinutes = 30d;
private VisibleDateRange _currentRange = new(
CalendarDisplayType.Month,
DateOnly.FromDateTime(DateTime.Today),
@@ -62,6 +67,7 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
public CalendarPeriodControl() => InitializeComponent();
public event PropertyChangedEventHandler? PropertyChanged;
public event EventHandler<CalendarEmptySlotTappedEventArgs>? EmptySlotTapped;
private ObservableCollection<HeaderTextLayout> TimedHeaderTextsCollection { get; } = [];
private ObservableCollection<HeaderTextLayout> MonthHeaderTextsCollection { get; } = [];
@@ -209,20 +215,22 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
{
TimedRoot.Visibility = Visibility.Visible;
MonthRoot.Visibility = Visibility.Collapsed;
ResetTimedVisualState();
TimedDayWidth = _currentRange.Dates.Count == 0 ? 0d : ActualWidth / _currentRange.Dates.Count;
TimedViewport.Width = ActualWidth;
var timedSurfaceWidth = GetTimedSurfaceWidth();
TimedDayWidth = _currentRange.Dates.Count == 0 ? 0d : timedSurfaceWidth / _currentRange.Dates.Count;
TimedScrollContentGrid.Width = ActualWidth;
TimedViewport.Width = timedSurfaceWidth;
TimedViewport.Height = TimelineHeight;
_timedLayout = TimedCalendarLayoutCalculator.Calculate(_currentRange, CurrentItems, ActualWidth, GetHourHeight());
_timedLayout = TimedCalendarLayoutCalculator.Calculate(_currentRange, CurrentItems, timedSurfaceWidth, GetHourHeight());
ReplaceCollection(
TimedHeaderTextsCollection,
_timedLayout.VisibleDates.Select(date =>
new HeaderTextLayout(
date.ToDateTime(TimeOnly.MinValue).ToString(
string.IsNullOrWhiteSpace(TimedHeaderDateFormat) ? "ddd dd" : TimedHeaderDateFormat,
CalendarSettings!.CultureInfo),
GetTimedHeaderText(date),
TimedDayWidth)));
var eventTemplate = (DataTemplate)Resources["CalendarEventTemplate"];
@@ -232,6 +240,7 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
item.Template = eventTemplate;
return item;
}));
RenderHourLabels();
RenderTimedItems();
TimedHeaderCanvas.Invalidate();
@@ -352,12 +361,14 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
var canvas = e.Surface.Canvas;
canvas.Clear(SKColors.Transparent);
if (_timedLayout.VisibleDates.Count == 0 || ActualWidth <= 0)
var timedSurfaceWidth = GetTimedSurfaceWidth();
if (_timedLayout.VisibleDates.Count == 0 || timedSurfaceWidth <= 0)
{
return;
}
var scaleX = (float)(e.Info.Width / ActualWidth);
var scaleX = (float)(e.Info.Width / timedSurfaceWidth);
var height = e.Info.Height;
var dayWidth = (float)(_timedLayout.DayWidth * scaleX);
@@ -373,41 +384,50 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
private void TimedStructureCanvasPaintSurface(object? sender, SKPaintSurfaceEventArgs e)
{
using var linePaint = CreateLinePaint();
using var minorLinePaint = CreateMinorLinePaint();
using var defaultFillPaint = CreateFillPaint(GetDefaultHourBackground());
using var workFillPaint = CreateFillPaint(GetWorkHourBackground());
var canvas = e.Surface.Canvas;
canvas.Clear(SKColors.Transparent);
if (_timedLayout.VisibleDates.Count == 0 || ActualWidth <= 0)
var timedSurfaceWidth = GetTimedSurfaceWidth();
if (_timedLayout.VisibleDates.Count == 0 || timedSurfaceWidth <= 0)
{
return;
}
var hourHeight = GetHourHeight();
var timelineHeight = TimedCalendarLayoutCalculator.GetTimelineHeight(hourHeight);
var scaleX = (float)(e.Info.Width / ActualWidth);
var scaleX = (float)(e.Info.Width / timedSurfaceWidth);
var scaleY = (float)(e.Info.Height / timelineHeight);
var dayWidth = (float)(_timedLayout.DayWidth * scaleX);
var workDayStartHour = CalendarSettings?.WorkingHourStart.TotalHours ?? 9d;
var workDayEndHour = CalendarSettings?.WorkingHourEnd.TotalHours ?? 17d;
var intervalHeight = (float)(GetTimedGridIntervalHeight() * scaleY);
var intervalCount = (int)(24d * 60d / TimedGridIntervalMinutes);
for (var dayIndex = 0; dayIndex < _timedLayout.VisibleDates.Count; dayIndex++)
{
var x = dayIndex * dayWidth;
var isWorkingDay = CalendarSettings?.WorkingDays.Contains(_timedLayout.VisibleDates[dayIndex].DayOfWeek) == true;
for (var hour = 0; hour < 24; hour++)
for (var intervalIndex = 0; intervalIndex < intervalCount; intervalIndex++)
{
var y = (float)(hour * hourHeight * scaleY);
var scaledHourHeight = (float)(hourHeight * scaleY);
var fillPaint = hour >= workDayStartHour && hour < workDayEndHour ? workFillPaint : defaultFillPaint;
canvas.DrawRect(x, y, dayWidth, scaledHourHeight, fillPaint);
var intervalStartHour = (intervalIndex * TimedGridIntervalMinutes) / 60d;
var y = intervalIndex * intervalHeight;
var fillPaint = isWorkingDay && intervalStartHour >= workDayStartHour && intervalStartHour < workDayEndHour
? workFillPaint
: defaultFillPaint;
canvas.DrawRect(x, y, dayWidth, intervalHeight, fillPaint);
}
}
for (var hour = 0; hour <= 24; hour++)
for (var intervalIndex = 0; intervalIndex <= intervalCount; intervalIndex++)
{
var y = (float)(hour * hourHeight * scaleY);
canvas.DrawLine(0, y, e.Info.Width, y, linePaint);
var y = intervalIndex * intervalHeight;
var paint = intervalIndex % 2 == 0 ? linePaint : minorLinePaint;
canvas.DrawLine(0, y, e.Info.Width, y, paint);
}
for (var index = 0; index <= _timedLayout.VisibleDates.Count; index++)
@@ -472,6 +492,34 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
}
}
private void RenderHourLabels()
{
HourLabelsCanvas.Children.Clear();
HourLabelsCanvas.Height = TimelineHeight;
var hourHeight = GetHourHeight();
var labelWidth = Math.Max(0d, TimedHourColumnWidth - 10d);
for (var hour = 0; hour <= 24; hour++)
{
var textBlock = new TextBlock
{
Width = labelWidth,
Text = GetTimedHourLabelText(hour),
TextAlignment = TextAlignment.Right,
Opacity = 0.72
};
var y = hour == 24
? Math.Max(0d, TimelineHeight - 20d)
: Math.Max(0d, (hour * hourHeight) - 10d);
Canvas.SetLeft(textBlock, 0d);
Canvas.SetTop(textBlock, y);
HourLabelsCanvas.Children.Add(textBlock);
}
}
private void RenderTimedItems()
{
TimedItemsCanvas.Children.Clear();
@@ -532,6 +580,70 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
}
}
private void TimedInteractionLayerTapped(object sender, TappedRoutedEventArgs e)
{
if (_timedLayout.VisibleDates.Count == 0 || _timedLayout.DayWidth <= 0)
{
return;
}
var position = e.GetPosition(TimedViewport);
var dayIndex = Math.Clamp((int)(position.X / _timedLayout.DayWidth), 0, _timedLayout.VisibleDates.Count - 1);
var intervalHeight = GetTimedSelectionIntervalHeight();
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);
EmptySlotTapped?.Invoke(
this,
new CalendarEmptySlotTappedEventArgs(
clickedDate,
new Point(dayIndex * _timedLayout.DayWidth, slotIndex * intervalHeight),
new Size(_timedLayout.DayWidth, intervalHeight)));
}
private void MonthInteractionLayerTapped(object sender, TappedRoutedEventArgs e)
{
if (_monthLayout.Cells.Count == 0 || _monthLayout.CellWidth <= 0 || _monthLayout.CellHeight <= 0)
{
return;
}
var position = e.GetPosition(MonthViewport);
var column = Math.Clamp((int)(position.X / _monthLayout.CellWidth), 0, MonthCalendarLayoutCalculator.ColumnCount - 1);
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];
EmptySlotTapped?.Invoke(
this,
new CalendarEmptySlotTappedEventArgs(
cell.Date.ToDateTime(TimeOnly.MinValue),
new Point(cell.Bounds.X, cell.Bounds.Y),
new Size(cell.Bounds.Width, cell.Bounds.Height)));
}
private double GetTimedSurfaceWidth() => Math.Max(0d, ActualWidth - TimedHourColumnWidth);
private string GetTimedHeaderText(DateOnly date)
{
if (!string.IsNullOrWhiteSpace(TimedHeaderDateFormat) && CalendarSettings is not null)
{
try
{
return date.ToDateTime(TimeOnly.MinValue).ToString(TimedHeaderDateFormat, CalendarSettings.CultureInfo);
}
catch (FormatException)
{
}
}
return CalendarSettings?.GetTimedDayHeaderText(date) ?? date.ToDateTime(TimeOnly.MinValue).ToString("ddd dd");
}
private string GetTimedHourLabelText(int hour)
=> CalendarSettings?.GetTimedHourLabelText(hour) ?? $"{hour:00}:00";
private CalendarTransitionInfo GetTransitionInfo()
{
if (!_hasPresentedState || VisibleRange is null || CalendarSettings is null)
@@ -597,11 +709,9 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
private void RunTimedTransition(CalendarTransitionInfo transition)
{
var headerVisual = ElementCompositionPreview.GetElementVisual(TimedHeaderHost);
var contentVisual = ElementCompositionPreview.GetElementVisual(TimedScrollViewer);
var compositor = headerVisual.Compositor;
var compositor = contentVisual.Compositor;
PrepareAnimatedVisual(headerVisual, TimedHeaderHost);
PrepareAnimatedVisual(contentVisual, TimedScrollViewer);
switch (transition.Kind)
@@ -618,6 +728,11 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
}
}
private void ResetTimedVisualState()
{
ResetAnimatedElement(TimedScrollViewer);
}
private static void StartNavigationTransition(Compositor compositor, Visual visual, int direction, double width)
{
var travel = (float)Math.Max(48d, Math.Min(160d, width * 0.08d));
@@ -644,8 +759,7 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
var signedTravel = direction >= 0 ? travel : -travel;
var clipInset = (float)Math.Max(18d, Math.Min(64d, width * 0.05d));
StartTimedElementTransition(compositor, TimedHeaderHost, signedTravel * 0.45f, 0f, 0.78f, TimeSpan.FromMilliseconds(180), direction >= 0 ? 0f : clipInset, direction >= 0 ? clipInset : 0f);
StartTimedElementTransition(compositor, TimedScrollViewer, signedTravel, 0f, 0.68f, TimeSpan.FromMilliseconds(240), direction >= 0 ? 0f : clipInset, direction >= 0 ? clipInset : 0f);
StartTimedElementTransition(compositor, TimedScrollViewer, signedTravel, 0f, 0.68f, TimeSpan.FromMilliseconds(240), direction >= 0 ? 0f : clipInset, direction >= 0 ? clipInset : 0f, animateScale: false);
}
private static void StartModeTransition(Compositor compositor, Visual visual)
@@ -672,8 +786,7 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
private void StartTimedModeTransition(Compositor compositor)
{
StartTimedElementTransition(compositor, TimedHeaderHost, 0f, 10f, 0f, TimeSpan.FromMilliseconds(180), 0f, 0f);
StartTimedElementTransition(compositor, TimedScrollViewer, 0f, 18f, 0f, TimeSpan.FromMilliseconds(240), 0f, 0f);
StartTimedElementTransition(compositor, TimedScrollViewer, 0f, 18f, 0f, TimeSpan.FromMilliseconds(240), 0f, 0f, animateScale: false);
}
private static void StartRefreshTransition(Compositor compositor, Visual visual)
@@ -688,7 +801,6 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
private void StartTimedRefreshTransition(Compositor compositor)
{
StartOpacityTransition(compositor, ElementCompositionPreview.GetElementVisual(TimedHeaderHost), 0.86f, TimeSpan.FromMilliseconds(140));
StartOpacityTransition(compositor, ElementCompositionPreview.GetElementVisual(TimedScrollViewer), 0.8f, TimeSpan.FromMilliseconds(160));
}
@@ -700,7 +812,25 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
visual.StopAnimation(nameof(visual.Scale));
}
private static void StartTimedElementTransition(Compositor compositor, UIElement target, float offsetX, float offsetY, float startingOpacity, TimeSpan duration, float leftInset, float rightInset)
private static void ResetAnimatedElement(UIElement target)
{
var visual = ElementCompositionPreview.GetElementVisual(target);
PrepareAnimatedVisual(visual, target);
visual.Offset = Vector3.Zero;
visual.Opacity = 1f;
visual.Scale = new Vector3(1f, 1f, 1f);
if (visual.Clip is InsetClip clip)
{
clip.StopAnimation(nameof(clip.LeftInset));
clip.StopAnimation(nameof(clip.RightInset));
clip.LeftInset = 0f;
clip.RightInset = 0f;
}
}
private static void StartTimedElementTransition(Compositor compositor, UIElement target, float offsetX, float offsetY, float startingOpacity, TimeSpan duration, float leftInset, float rightInset, bool animateScale = true)
{
var visual = ElementCompositionPreview.GetElementVisual(target);
PrepareAnimatedVisual(visual, target);
@@ -723,11 +853,6 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
opacityAnimation.InsertKeyFrame(1f, 1f, fadeEasing);
opacityAnimation.Duration = duration;
var scaleAnimation = compositor.CreateVector3KeyFrameAnimation();
scaleAnimation.InsertKeyFrame(0f, new Vector3(0.996f, 0.996f, 1f));
scaleAnimation.InsertKeyFrame(1f, new Vector3(1f, 1f, 1f), easing);
scaleAnimation.Duration = duration;
var leftInsetAnimation = compositor.CreateScalarKeyFrameAnimation();
leftInsetAnimation.InsertKeyFrame(1f, 0f, easing);
leftInsetAnimation.Duration = duration;
@@ -738,7 +863,20 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
visual.StartAnimation(nameof(visual.Offset), offsetAnimation);
visual.StartAnimation(nameof(visual.Opacity), opacityAnimation);
if (animateScale)
{
var scaleAnimation = compositor.CreateVector3KeyFrameAnimation();
scaleAnimation.InsertKeyFrame(0f, new Vector3(0.996f, 0.996f, 1f));
scaleAnimation.InsertKeyFrame(1f, new Vector3(1f, 1f, 1f), easing);
scaleAnimation.Duration = duration;
visual.StartAnimation(nameof(visual.Scale), scaleAnimation);
}
else
{
visual.Scale = new Vector3(1f, 1f, 1f);
}
clip.StartAnimation(nameof(clip.LeftInset), leftInsetAnimation);
clip.StartAnimation(nameof(clip.RightInset), rightInsetAnimation);
}
@@ -768,6 +906,18 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
};
}
private static SKPaint CreateMinorLinePaint()
{
var strokeColor = GetStrokeColor();
return new SKPaint
{
Color = new SKColor(strokeColor.R, strokeColor.G, strokeColor.B, (byte)Math.Max(20, strokeColor.A / 4)),
IsAntialias = false,
StrokeWidth = 1
};
}
private static SKPaint CreateFillPaint(Brush brush)
{
return new SKPaint
@@ -805,6 +955,12 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
return new SolidColorBrush(Color.FromArgb(255, 34, 40, 52));
}
private static double GetTimedGridIntervalHeight(double hourHeight) => hourHeight * (TimedGridIntervalMinutes / 60d);
private double GetTimedGridIntervalHeight() => GetTimedGridIntervalHeight(GetHourHeight());
private double GetTimedSelectionIntervalHeight() => GetHourHeight() * (TimedSelectionIntervalMinutes / 60d);
private double GetHourHeight() => CalendarSettings?.HourHeight ?? 60d;
private static Color GetStrokeColor()
@@ -1,150 +0,0 @@
using System;
using System.Collections.Specialized;
using System.Linq;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Wino.Core.Domain.Collections;
using Wino.Core.Domain.Models.Calendar;
namespace Wino.Calendar.Controls;
public partial class DayColumnControl : Control
{
private const string PART_HeaderDateDayText = nameof(PART_HeaderDateDayText);
private const string PART_IsTodayBorder = nameof(PART_IsTodayBorder);
private const string PART_ColumnHeaderText = nameof(PART_ColumnHeaderText);
private const string PART_AllDayItemsControl = nameof(PART_AllDayItemsControl);
private const string TodayState = nameof(TodayState);
private const string NotTodayState = nameof(NotTodayState);
private TextBlock? HeaderDateDayText;
private TextBlock? ColumnHeaderText;
private Border? IsTodayBorder;
private ItemsControl? AllDayItemsControl;
private CalendarEventCollection? _boundEventsCollection;
public CalendarDayModel DayModel
{
get { return (CalendarDayModel)GetValue(DayModelProperty); }
set { SetValue(DayModelProperty, value); }
}
public static readonly DependencyProperty DayModelProperty = DependencyProperty.Register(
nameof(DayModel),
typeof(CalendarDayModel),
typeof(DayColumnControl),
new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged)));
public DayColumnControl()
{
DefaultStyleKey = typeof(DayColumnControl);
Unloaded += OnUnloaded;
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
HeaderDateDayText = GetTemplateChild(PART_HeaderDateDayText) as TextBlock;
ColumnHeaderText = GetTemplateChild(PART_ColumnHeaderText) as TextBlock;
IsTodayBorder = GetTemplateChild(PART_IsTodayBorder) as Border;
AllDayItemsControl = GetTemplateChild(PART_AllDayItemsControl) as ItemsControl;
RegisterEventsCollectionHandlers();
UpdateValues();
}
private static void OnRenderingPropertiesChanged(DependencyObject control, DependencyPropertyChangedEventArgs e)
{
if (control is DayColumnControl columnControl)
{
columnControl.RegisterEventsCollectionHandlers();
columnControl.UpdateValues();
}
}
private void OnUnloaded(object sender, RoutedEventArgs e)
{
DeregisterEventsCollectionHandlers();
}
private bool IsMonthlyTemplate() => ColumnHeaderText == null;
private void RegisterEventsCollectionHandlers()
{
var nextCollection = DayModel?.EventsCollection;
if (ReferenceEquals(_boundEventsCollection, nextCollection))
return;
DeregisterEventsCollectionHandlers();
_boundEventsCollection = nextCollection;
if (_boundEventsCollection == null)
return;
((INotifyCollectionChanged)_boundEventsCollection.AllDayEvents).CollectionChanged += EventsCollectionChanged;
((INotifyCollectionChanged)_boundEventsCollection.RegularEvents).CollectionChanged += EventsCollectionChanged;
}
private void DeregisterEventsCollectionHandlers()
{
if (_boundEventsCollection == null)
return;
((INotifyCollectionChanged)_boundEventsCollection.AllDayEvents).CollectionChanged -= EventsCollectionChanged;
((INotifyCollectionChanged)_boundEventsCollection.RegularEvents).CollectionChanged -= EventsCollectionChanged;
_boundEventsCollection = null;
}
private void EventsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
UpdateEventItemsSource();
}
private void UpdateEventItemsSource()
{
if (AllDayItemsControl == null || DayModel == null) return;
if (IsMonthlyTemplate())
{
// Month cells should show all events for the day, not only all-day/multi-day.
var monthlyItems = DayModel.EventsCollection.AllDayEvents
.Concat(DayModel.EventsCollection.RegularEvents)
.GroupBy(a => a.Id)
.Select(g => g.First())
.OrderBy(a => a.StartDate)
.ToList();
AllDayItemsControl.ItemsSource = monthlyItems;
return;
}
AllDayItemsControl.ItemsSource = DayModel.EventsCollection.AllDayEvents;
}
private void UpdateValues()
{
if (DayModel == null) return;
if (HeaderDateDayText != null)
{
HeaderDateDayText.Text = DayModel.RepresentingDate.Day.ToString();
}
// Monthly template does not use it.
if (ColumnHeaderText != null)
{
ColumnHeaderText.Text = DayModel.RepresentingDate.ToString("dddd", DayModel.CalendarRenderOptions.CalendarSettings.CultureInfo);
}
UpdateEventItemsSource();
if (IsTodayBorder == null) return;
bool isToday = DayModel.RepresentingDate.Date == DateTime.Now.Date;
VisualStateManager.GoToState(this, isToday ? TodayState : NotTodayState, false);
UpdateLayout();
}
}
@@ -1,56 +0,0 @@
using System;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Wino.Core.Domain.Enums;
namespace Wino.Calendar.Controls;
public partial class DayHeaderControl : Control
{
private const string PART_DayHeaderTextBlock = nameof(PART_DayHeaderTextBlock);
private TextBlock? HeaderTextblock;
public DayHeaderDisplayType DisplayType
{
get { return (DayHeaderDisplayType)GetValue(DisplayTypeProperty); }
set { SetValue(DisplayTypeProperty, value); }
}
public DateTime Date
{
get { return (DateTime)GetValue(DateProperty); }
set { SetValue(DateProperty, value); }
}
public static readonly DependencyProperty DateProperty = DependencyProperty.Register(nameof(Date), typeof(DateTime), typeof(DayHeaderControl), new PropertyMetadata(default(DateTime), new PropertyChangedCallback(OnHeaderPropertyChanged)));
public static readonly DependencyProperty DisplayTypeProperty = DependencyProperty.Register(nameof(DisplayType), typeof(DayHeaderDisplayType), typeof(DayHeaderControl), new PropertyMetadata(DayHeaderDisplayType.TwentyFourHour, new PropertyChangedCallback(OnHeaderPropertyChanged)));
public DayHeaderControl()
{
DefaultStyleKey = typeof(DayHeaderControl);
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
HeaderTextblock = GetTemplateChild(PART_DayHeaderTextBlock) as TextBlock;
UpdateHeaderText();
}
private static void OnHeaderPropertyChanged(DependencyObject control, DependencyPropertyChangedEventArgs e)
{
if (control is DayHeaderControl headerControl)
{
headerControl.UpdateHeaderText();
}
}
private void UpdateHeaderText()
{
if (HeaderTextblock != null)
{
HeaderTextblock.Text = DisplayType == DayHeaderDisplayType.TwelveHour ? Date.ToString("h tt") : Date.ToString("HH:mm");
}
}
}
@@ -1,85 +0,0 @@
using System.Collections.Generic;
using CommunityToolkit.WinUI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Wino.Calendar.Controls;
using Wino.Core.Domain.Models.Calendar;
namespace Wino.Mail.WinUI.Controls.Calendar;
/// <summary>
/// AOT-Safe ItemsControl for use in UniformGrid panels.
/// </summary>
///
public partial class UniformItemsControl : Grid
{
[GeneratedDependencyProperty]
public partial DayRangeRenderModel? RenderModel { get; set; }
[GeneratedDependencyProperty]
public partial List<CalendarDayModel>? ItemsSource { get; set; }
partial void OnRenderModelChanged(DayRangeRenderModel? newValue)
{
if (newValue == null || ItemsSource == null) return;
AdjustColumns();
}
partial void OnItemsSourceChanged(List<CalendarDayModel>? newValue)
{
if (newValue == null || ItemsSource == null) return;
AdjustColumns();
}
private void AdjustColumns()
{
if (RenderModel == null || ItemsSource == null) return;
Children.Clear();
ColumnDefinitions.Clear();
var columns = RenderModel.TotalDays;
// First divide.
for (int i = 0; i < columns; i++)
{
ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
}
// Then add items.
for (int i = 0; i < columns; i++)
{
var item = ItemsSource[i];
var control = new DayColumnControl()
{
DayModel = item
};
SetColumn(control, i);
Children.Add(control);
}
}
}
//public partial class UniformItemsControl : ItemsControl
//{
// private const string ControlUniformGridName = "PART_UniformGrid";
// [GeneratedDependencyProperty]
// public partial DayRangeRenderModel? RenderModel { get; set; }
// partial void OnRenderModelChanged(DayRangeRenderModel? newValue)
// {
// if (newValue == null) return;
// // Adjust the ItemsPanel based on the RenderModel's columns.
// var uniGrid = WinoVisualTreeHelper.FindDescendants<UniformGrid>(this);
// //if (uniGrid != null)
// //{
// // uniGrid.Columns = newValue.TotalDays;
// //}
// }
//}
@@ -1,293 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CommunityToolkit.WinUI;
using Itenso.TimePeriod;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Windows.Foundation;
using Wino.Calendar.Models;
using Wino.Calendar.ViewModels.Data;
using Wino.Core.Domain.Interfaces;
namespace Wino.Calendar.Controls;
public partial class WinoCalendarPanel : Panel
{
private const double LastItemRightExtraMargin = 12d;
// Store each ICalendarItem measurements by their Id.
private readonly Dictionary<ICalendarItem, CalendarItemMeasurement> _measurements = new Dictionary<ICalendarItem, CalendarItemMeasurement>();
public static readonly DependencyProperty EventItemMarginProperty = DependencyProperty.Register(nameof(EventItemMargin), typeof(Thickness), typeof(WinoCalendarPanel), new PropertyMetadata(new Thickness(0, 0, 0, 0)));
public static readonly DependencyProperty HourHeightProperty = DependencyProperty.Register(nameof(HourHeight), typeof(double), typeof(WinoCalendarPanel), new PropertyMetadata(0d));
public static readonly DependencyProperty PeriodProperty = DependencyProperty.Register(nameof(Period), typeof(ITimePeriod), typeof(WinoCalendarPanel), new PropertyMetadata(null));
public ITimePeriod Period
{
get { return (ITimePeriod)GetValue(PeriodProperty); }
set { SetValue(PeriodProperty, value); }
}
public double HourHeight
{
get { return (double)GetValue(HourHeightProperty); }
set { SetValue(HourHeightProperty, value); }
}
public Thickness EventItemMargin
{
get { return (Thickness)GetValue(EventItemMarginProperty); }
set { SetValue(EventItemMarginProperty, value); }
}
private void ResetMeasurements() => _measurements.Clear();
private double GetChildTopMargin(ICalendarItem calendarItemViewModel, double availableHeight)
{
var childStart = calendarItemViewModel.StartDate;
if (childStart <= Period.Start)
{
// Event started before or exactly at the periods tart. This might be a multi-day event.
// We can simply consider event must not have a top margin.
return 0d;
}
double minutesFromStart = (childStart - Period.Start).TotalMinutes;
return (minutesFromStart / 1440) * availableHeight;
}
private double GetChildWidth(CalendarItemMeasurement calendarItemMeasurement, double availableWidth)
{
return (calendarItemMeasurement.Right - calendarItemMeasurement.Left) * availableWidth;
}
private double GetChildLeftMargin(CalendarItemMeasurement calendarItemMeasurement, double availableWidth)
=> availableWidth * calendarItemMeasurement.Left;
private double GetChildHeight(ICalendarItem child)
{
// All day events are not measured.
if (child.IsAllDayEvent) return 0;
double childDurationInMinutes = 0d;
double availableHeight = HourHeight * 24;
var periodRelation = child.Period.GetRelation(Period);
// Debug.WriteLine($"Render relation of {child.Title} ({child.Period.Start} - {child.Period.End}) is {periodRelation} with {Period.Start.Day}");
if (!child.IsMultiDayEvent)
{
childDurationInMinutes = child.Period.Duration.TotalMinutes;
}
else
{
// Multi-day event.
// Check how many of the event falls into the current period.
childDurationInMinutes = (child.Period.End - Period.Start).TotalMinutes;
}
return (childDurationInMinutes / 1440) * availableHeight;
}
protected override Size MeasureOverride(Size availableSize)
{
ResetMeasurements();
return base.MeasureOverride(availableSize);
}
protected override Size ArrangeOverride(Size finalSize)
{
if (Period == null || HourHeight == 0d) return finalSize;
// Measure/arrange each child height and width.
// This is a vertical calendar. Therefore the height of each child is the duration of the event.
// Children weights for left and right will be saved if they don't exist.
// This is important because we don't want to measure the weights again.
// They don't change until new event is added or removed.
// Width of the each child may depend on the rectangle packing algorithm.
// Children are first categorized into columns. Then each column is shifted to the left until
// no overlap occurs. The width of each child is calculated based on the number of columns it spans.
double availableHeight = finalSize.Height;
double availableWidth = finalSize.Width;
var calendarControls = Children.Cast<ContentPresenter>();
if (!calendarControls.Any()) return base.ArrangeOverride(finalSize);
var events = calendarControls.Select(a => a.Content as CalendarItemViewModel).OfType<ICalendarItem>();
LayoutEvents(events);
foreach (var control in calendarControls)
{
// We can't arrange this child.
if (!(control.Content is ICalendarItem child)) continue;
bool isHorizontallyLastItem = false;
double childWidth = 0,
childHeight = Math.Max(0, GetChildHeight(child)),
childTop = Math.Max(0, GetChildTopMargin(child, availableHeight)),
childLeft = 0;
// No need to measure anything here.
if (childHeight == 0) continue;
if (!_measurements.ContainsKey(child))
{
// Multi-day event.
childLeft = 0;
childWidth = availableWidth;
}
else
{
var childMeasurement = _measurements[child];
childWidth = Math.Max(0, GetChildWidth(childMeasurement, finalSize.Width));
childLeft = Math.Max(0, GetChildLeftMargin(childMeasurement, availableWidth));
isHorizontallyLastItem = childMeasurement.Right == 1;
}
// Add additional right margin to items that falls on the right edge of the panel.
double extraRightMargin = 0;
// Multi-day events don't have any margin and their hit test is disabled.
if (!child.IsMultiDayEvent)
{
// Max of 5% of the width or 20px max.
extraRightMargin = isHorizontallyLastItem ? Math.Max(LastItemRightExtraMargin, finalSize.Width * 5 / 100) : 0;
}
if (childWidth < 0) childWidth = 1;
// Regular events must have 2px margin
if (!child.IsMultiDayEvent && !child.IsAllDayEvent)
{
childLeft += 2;
childTop += 2;
childHeight -= 2;
childWidth -= 2;
}
var arrangementRect = new Rect(childLeft + EventItemMargin.Left, childTop + EventItemMargin.Top, Math.Max(childWidth - extraRightMargin, 1), childHeight);
// Make sure measured size will fit in the arranged box.
var measureSize = arrangementRect.ToSize();
control.Measure(measureSize);
control.Arrange(arrangementRect);
//Debug.WriteLine($"{child.Title}, Measured: {measureSize}, Arranged: {arrangementRect}");
}
return finalSize;
}
#region ColumSpanning and Packing Algorithm
private void AddOrUpdateMeasurement(ICalendarItem calendarItem, CalendarItemMeasurement measurement)
{
if (_measurements.ContainsKey(calendarItem))
{
_measurements[calendarItem] = measurement;
}
else
{
_measurements.Add(calendarItem, measurement);
}
}
// Pick the left and right positions of each event, such that there are no overlap.
private void LayoutEvents(IEnumerable<ICalendarItem> events)
{
var columns = new List<List<ICalendarItem>>();
DateTime? lastEventEnding = null;
foreach (var ev in events.OrderBy(ev => ev.StartDate).ThenBy(ev => ev.EndDate))
{
// Multi-day events are not measured.
if (ev.IsMultiDayEvent) continue;
if (ev.Period.Start >= lastEventEnding)
{
PackEvents(columns);
columns.Clear();
lastEventEnding = null;
}
bool placed = false;
foreach (var col in columns)
{
if (!col.Last().Period.OverlapsWith(ev.Period))
{
col.Add(ev);
placed = true;
break;
}
}
if (!placed)
{
columns.Add(new List<ICalendarItem> { ev });
}
if (lastEventEnding == null || ev.Period.End > lastEventEnding.Value)
{
lastEventEnding = ev.Period.End;
}
}
if (columns.Count > 0)
{
PackEvents(columns);
}
}
// Set the left and right positions for each event in the connected group.
private void PackEvents(List<List<ICalendarItem>> columns)
{
float numColumns = columns.Count;
int iColumn = 0;
foreach (var col in columns)
{
foreach (var ev in col)
{
int colSpan = ExpandEvent(ev, iColumn, columns);
var leftWeight = iColumn / numColumns;
var rightWeight = (iColumn + colSpan) / numColumns;
AddOrUpdateMeasurement(ev, new CalendarItemMeasurement(leftWeight, rightWeight));
}
iColumn++;
}
}
// Checks how many columns the event can expand into, without colliding with other events.
private int ExpandEvent(ICalendarItem ev, int iColumn, List<List<ICalendarItem>> columns)
{
int colSpan = 1;
foreach (var col in columns.Skip(iColumn + 1))
{
foreach (var ev1 in col)
{
if (ev1.Period.OverlapsWith(ev.Period)) return colSpan;
}
colSpan++;
}
return colSpan;
}
#endregion
}
@@ -1,8 +1,9 @@
using System.Windows.Input;
using System.Windows.Input;
using CommunityToolkit.Diagnostics;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Wino.Core.Domain.Enums;
using Wino.Mail.WinUI.Controls;
namespace Wino.Calendar.Controls;
@@ -11,6 +12,7 @@ public partial class WinoCalendarTypeSelectorControl : Control
private const string PART_TodayButton = nameof(PART_TodayButton);
private const string PART_DayToggle = nameof(PART_DayToggle);
private const string PART_WeekToggle = nameof(PART_WeekToggle);
private const string PART_WorkWeekToggle = nameof(PART_WorkWeekToggle);
private const string PART_MonthToggle = nameof(PART_MonthToggle);
public static readonly DependencyProperty SelectedTypeProperty = DependencyProperty.Register(
@@ -42,6 +44,7 @@ public partial class WinoCalendarTypeSelectorControl : Control
private AppBarButton? _todayButton;
private AppBarToggleButton? _dayToggle;
private AppBarToggleButton? _weekToggle;
private AppBarToggleButton? _workWeekToggle;
private AppBarToggleButton? _monthToggle;
public WinoCalendarTypeSelectorControl()
@@ -58,24 +61,34 @@ public partial class WinoCalendarTypeSelectorControl : Control
_todayButton = GetTemplateChild(PART_TodayButton) as AppBarButton;
_dayToggle = GetTemplateChild(PART_DayToggle) as AppBarToggleButton;
_weekToggle = GetTemplateChild(PART_WeekToggle) as AppBarToggleButton;
_workWeekToggle = GetTemplateChild(PART_WorkWeekToggle) as AppBarToggleButton;
_monthToggle = GetTemplateChild(PART_MonthToggle) as AppBarToggleButton;
Guard.IsNotNull(_todayButton, nameof(_todayButton));
Guard.IsNotNull(_dayToggle, nameof(_dayToggle));
Guard.IsNotNull(_weekToggle, nameof(_weekToggle));
Guard.IsNotNull(_workWeekToggle, nameof(_workWeekToggle));
Guard.IsNotNull(_monthToggle, nameof(_monthToggle));
_todayButton!.Click += TodayClicked;
_dayToggle!.Click += (s, e) => { SetSelectedType(CalendarDisplayType.Day); };
_weekToggle!.Click += (s, e) => { SetSelectedType(CalendarDisplayType.Week); };
_monthToggle!.Click += (s, e) => { SetSelectedType(CalendarDisplayType.Month); };
_dayToggle!.Click += DayToggleClicked;
_weekToggle!.Click += WeekToggleClicked;
_workWeekToggle!.Click += WorkWeekToggleClicked;
_monthToggle!.Click += MonthToggleClicked;
UpdateToggleButtonStates();
}
private void TodayClicked(object? sender, RoutedEventArgs e) => TodayClickedCommand?.Execute(null);
private void DayToggleClicked(object sender, RoutedEventArgs e) => SetSelectedType(CalendarDisplayType.Day);
private void WeekToggleClicked(object sender, RoutedEventArgs e) => SetSelectedType(CalendarDisplayType.Week);
private void WorkWeekToggleClicked(object sender, RoutedEventArgs e) => SetSelectedType(CalendarDisplayType.WorkWeek);
private void MonthToggleClicked(object sender, RoutedEventArgs e) => SetSelectedType(CalendarDisplayType.Month);
private void SetSelectedType(CalendarDisplayType type)
{
SelectedType = type;
@@ -84,8 +97,10 @@ public partial class WinoCalendarTypeSelectorControl : Control
private static void OnSelectedTypeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = d as WinoCalendarTypeSelectorControl;
control?.UpdateToggleButtonStates();
if (d is WinoCalendarTypeSelectorControl control)
{
control.UpdateToggleButtonStates();
}
}
private void UnregisterHandlers()
@@ -94,17 +109,38 @@ public partial class WinoCalendarTypeSelectorControl : Control
{
_todayButton.Click -= TodayClicked;
}
if (_dayToggle != null)
{
_dayToggle.Click -= DayToggleClicked;
}
if (_weekToggle != null)
{
_weekToggle.Click -= WeekToggleClicked;
}
if (_workWeekToggle != null)
{
_workWeekToggle.Click -= WorkWeekToggleClicked;
}
if (_monthToggle != null)
{
_monthToggle.Click -= MonthToggleClicked;
}
}
private void UpdateToggleButtonStates()
{
if (_dayToggle == null || _weekToggle == null || _monthToggle == null)
if (_dayToggle == null || _weekToggle == null || _workWeekToggle == null || _monthToggle == null)
{
return;
}
_dayToggle.IsChecked = SelectedType == CalendarDisplayType.Day;
_weekToggle.IsChecked = SelectedType == CalendarDisplayType.Week;
_workWeekToggle.IsChecked = SelectedType == CalendarDisplayType.WorkWeek;
_monthToggle.IsChecked = SelectedType == CalendarDisplayType.Month;
}
}
@@ -76,6 +76,7 @@ public static class ControlConstants
{ WinoIconGlyph.CalendarToday, "\uE911" },
{ WinoIconGlyph.CalendarDay, "\uE913" },
{ WinoIconGlyph.CalendarWeek, "\uE914" },
{ WinoIconGlyph.CalendarWorkWeek, "\uE914" },
{ WinoIconGlyph.CalendarMonth, "\uE91c" },
{ WinoIconGlyph.CalendarYear, "\uE917" },
{ WinoIconGlyph.WeatherBlow, "\uE907" },
+1
View File
@@ -43,6 +43,7 @@ public static class XamlHelpers
public static Visibility ReverseBoolToVisibilityConverter(bool value) => value ? Visibility.Collapsed : Visibility.Visible;
public static Visibility ReverseVisibilityConverter(Visibility visibility) => visibility == Visibility.Visible ? Visibility.Collapsed : Visibility.Visible;
public static bool ReverseBoolConverter(bool value) => !value;
public static GridLength DoubleToGridLength(double value) => new(value);
public static bool AreEqual(int value1, int value2) => value1 == value2;
public static bool ShouldDisplayPreview(string text) => text == null ? false : text.Any(x => char.IsLetter(x));
public static bool CountToBooleanConverter(int value) => value > 0;
@@ -311,6 +311,12 @@ public class PreferencesService(IConfigurationService configurationService) : Ob
set => SaveProperty(propertyName: nameof(HourHeight), value);
}
public string CalendarTimedDayHeaderDateFormat
{
get => _configurationService.Get(nameof(CalendarTimedDayHeaderDateFormat), "ddd dd");
set => SaveProperty(propertyName: nameof(CalendarTimedDayHeaderDateFormat), string.IsNullOrWhiteSpace(value) ? "ddd dd" : value.Trim());
}
public TimeSpan WorkingHourStart
{
get => _configurationService.Get(nameof(WorkingHourStart), new TimeSpan(8, 0, 0));
@@ -402,7 +408,8 @@ public class PreferencesService(IConfigurationService configurationService) : Ob
WorkingHourEnd,
HourHeight,
Prefer24HourTimeFormat ? DayHeaderDisplayType.TwentyFourHour : DayHeaderDisplayType.TwelveHour,
new CultureInfo(WinoTranslationDictionary.GetLanguageFileNameRelativePath(CurrentLanguage)));
new CultureInfo(WinoTranslationDictionary.GetLanguageFileNameRelativePath(CurrentLanguage)),
CalendarTimedDayHeaderDateFormat);
}
private List<DayOfWeek> GetDaysBetween(DayOfWeek startDay, DayOfWeek endDay)
@@ -1,23 +0,0 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Wino.Calendar.Controls">
<!-- Left day header DayHeaderControl -->
<Style TargetType="controls:DayHeaderControl">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="controls:DayHeaderControl">
<Grid>
<TextBlock
x:Name="PART_DayHeaderTextBlock"
HorizontalAlignment="Center"
VerticalAlignment="Top"
FontSize="12" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
@@ -2,8 +2,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Wino.Calendar.Controls"
xmlns:controls1="using:Wino.Mail.WinUI.Controls"
xmlns:muxc="using:Microsoft.UI.Xaml.Controls">
xmlns:controls1="using:Wino.Mail.WinUI.Controls">
<Style TargetType="controls:WinoCalendarTypeSelectorControl">
<Style.Setters>
@@ -16,7 +15,6 @@
DefaultLabelPosition="Right">
<CommandBar.PrimaryCommands>
<!-- Today -->
<AppBarButton
x:Name="PART_TodayButton"
Foreground="{ThemeResource ApplicationForegroundThemeBrush}"
@@ -28,8 +26,6 @@
<AppBarSeparator />
<!-- Day -->
<!-- TODO: Specific days -->
<AppBarToggleButton
x:Name="PART_DayToggle"
Foreground="{ThemeResource ApplicationForegroundThemeBrush}"
@@ -39,9 +35,6 @@
</AppBarToggleButton.Icon>
</AppBarToggleButton>
<!-- Week -->
<!-- TODO: Work week -->
<AppBarToggleButton
x:Name="PART_WeekToggle"
Foreground="{ThemeResource ApplicationForegroundThemeBrush}"
@@ -51,7 +44,15 @@
</AppBarToggleButton.Icon>
</AppBarToggleButton>
<!-- Month -->
<AppBarToggleButton
x:Name="PART_WorkWeekToggle"
Foreground="{ThemeResource ApplicationForegroundThemeBrush}"
Label="Work Week">
<AppBarToggleButton.Icon>
<controls1:WinoFontIcon Icon="CalendarWorkWeek" />
</AppBarToggleButton.Icon>
</AppBarToggleButton>
<AppBarToggleButton
x:Name="PART_MonthToggle"
Foreground="{ThemeResource ApplicationForegroundThemeBrush}"
@@ -60,14 +61,11 @@
<controls1:WinoFontIcon FontSize="44" Icon="CalendarMonth" />
</AppBarToggleButton.Icon>
</AppBarToggleButton>
</CommandBar.PrimaryCommands>
</CommandBar>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style.Setters>
</Style>
</ResourceDictionary>
@@ -4,16 +4,224 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:abstract="using:Wino.Calendar.Views.Abstract"
xmlns:calendarControls="using:Wino.Calendar.Controls"
xmlns:collections="using:CommunityToolkit.Mvvm.Collections"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:data="using:Wino.Calendar.ViewModels.Data"
xmlns:domain="using:Wino.Core.Domain"
xmlns:helpers="using:Wino.Helpers"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Page.Resources>
<CollectionViewSource
x:Name="GroupedCalendarEnumerableViewSource"
IsSourceGrouped="True"
Source="{x:Bind ViewModel.AccountCalendarStateService.GroupedCalendars, Mode=OneWay}" />
</Page.Resources>
<Grid>
<Border
Margin="4,0,7,7"
BorderBrush="{StaticResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="7">
<calendarControls:CalendarPeriodControl
x:Name="CalendarSurface"
IsEnabled="{x:Bind ViewModel.IsCalendarEnabled, Mode=OneWay}"
CalendarItems="{x:Bind ViewModel.CalendarItems, Mode=OneWay}"
CalendarSettings="{x:Bind ViewModel.CurrentSettings, Mode=OneWay}"
EmptySlotTapped="CalendarSurfaceEmptySlotTapped"
IsEnabled="{x:Bind ViewModel.IsCalendarEnabled, Mode=OneWay}"
VisibleRange="{x:Bind ViewModel.CurrentVisibleRange, Mode=OneWay}" />
</Border>
<Canvas x:Name="CalendarOverlayCanvas" IsHitTestVisible="False">
<Grid
x:Name="TeachingTipPositionerGrid"
Background="Transparent"
IsHitTestVisible="False"
Visibility="Visible" />
<Popup
x:Name="QuickEventPopupDialog"
ActualPlacementChanged="PopupPlacementChanged"
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}">
<Popup.ChildTransitions>
<TransitionCollection>
<PopupThemeTransition />
</TransitionCollection>
</Popup.ChildTransitions>
<Grid
MinWidth="440"
MaxWidth="520"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
BorderBrush="{x:Bind helpers:XamlHelpers.GetSolidColorBrushFromHex(ViewModel.SelectedQuickEventAccountCalendar.BackgroundColorHex), Mode=OneWay, TargetNullValue='LightGray'}"
BorderThickness="1"
CornerRadius="8"
RowSpacing="12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Button
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
Background="{x:Bind helpers:XamlHelpers.GetSolidColorBrushFromHex(ViewModel.SelectedQuickEventAccountCalendar.BackgroundColorHex), Mode=OneWay, TargetNullValue='LightGray'}"
CornerRadius="8,8,0,0">
<Button.Content>
<Grid
Height="36"
Padding="12,0"
ColumnSpacing="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock
VerticalAlignment="Center"
Foreground="{x:Bind helpers:XamlHelpers.GetReadableTextColor(ViewModel.SelectedQuickEventAccountCalendar.BackgroundColorHex), Mode=OneWay}"
Text="{x:Bind ViewModel.SelectedQuickEventAccountCalendar.Account.Name, Mode=OneWay}" />
<TextBlock
Grid.Column="1"
VerticalAlignment="Center"
FontWeight="SemiBold"
Foreground="{x:Bind helpers:XamlHelpers.GetReadableTextColor(ViewModel.SelectedQuickEventAccountCalendar.BackgroundColorHex), Mode=OneWay}"
Text="{x:Bind ViewModel.SelectedQuickEventAccountCalendarName, Mode=OneWay}"
TextTrimming="CharacterEllipsis" />
<FontIcon
Grid.Column="2"
VerticalAlignment="Center"
Foreground="{x:Bind helpers:XamlHelpers.GetReadableTextColor(ViewModel.SelectedQuickEventAccountCalendar.BackgroundColorHex), Mode=OneWay}"
Glyph="&#xE70D;" />
</Grid>
</Button.Content>
<Button.Flyout>
<Flyout x:Name="QuickEventAccountSelectorFlyout" Placement="Bottom">
<ListView
MaxHeight="300"
ItemsSource="{x:Bind GroupedCalendarEnumerableViewSource.View, Mode=OneWay}"
SelectedItem="{x:Bind ViewModel.SelectedQuickEventAccountCalendar, Mode=TwoWay}"
SelectionChanged="QuickEventAccountSelectorSelectionChanged">
<ListView.ItemTemplate>
<DataTemplate x:DataType="data:AccountCalendarViewModel">
<StackPanel
Margin="0,0,16,0"
Orientation="Horizontal"
Spacing="8">
<Ellipse
Width="14"
Height="14"
Fill="{x:Bind helpers:XamlHelpers.GetSolidColorBrushFromHex(BackgroundColorHex), Mode=OneWay}" />
<TextBlock VerticalAlignment="Center" Text="{x:Bind Name, Mode=OneWay}" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
<ListView.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate x:DataType="collections:IReadOnlyObservableGroup">
<TextBlock FontWeight="SemiBold" Text="{x:Bind Key.ToString()}" />
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ListView.GroupStyle>
</ListView>
</Flyout>
</Button.Flyout>
</Button>
<Grid
Grid.Row="1"
Padding="12"
RowSpacing="16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox
FontSize="16"
PlaceholderText="{x:Bind domain:Translator.QuickEventDialog_EventName}"
Text="{x:Bind ViewModel.EventName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<CheckBox
Grid.Column="1"
VerticalAlignment="Center"
Content="{x:Bind domain:Translator.QuickEventDialog_IsAllDay}"
IsChecked="{x:Bind ViewModel.IsAllDay, Mode=TwoWay}" />
</Grid>
<Grid Grid.Row="1" ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ComboBox
IsEditable="True"
IsEnabled="{x:Bind helpers:XamlHelpers.ReverseBoolConverter(ViewModel.IsAllDay), Mode=OneWay}"
ItemsSource="{x:Bind ViewModel.HourSelectionStrings, Mode=OneWay}"
SelectedItem="{x:Bind ViewModel.SelectedStartTimeString, Mode=TwoWay}"
TextSubmitted="StartTimeDurationSubmitted" />
<TextBlock
Grid.Column="1"
VerticalAlignment="Center"
Text="-" />
<ComboBox
Grid.Column="2"
IsEditable="True"
IsEnabled="{x:Bind helpers:XamlHelpers.ReverseBoolConverter(ViewModel.IsAllDay), Mode=OneWay}"
ItemsSource="{x:Bind ViewModel.HourSelectionStrings, Mode=OneWay}"
SelectedItem="{x:Bind ViewModel.SelectedEndTimeString, Mode=TwoWay}"
TextSubmitted="EndTimeDurationSubmitted" />
</Grid>
<TextBox
Grid.Row="2"
PlaceholderText="{x:Bind domain:Translator.QuickEventDialog_Location}"
Text="{x:Bind ViewModel.Location, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<Grid Grid.Row="3" ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Command="{x:Bind ViewModel.MoreDetailsCommand}" -->
<Button HorizontalAlignment="Stretch" Content="{x:Bind domain:Translator.QuickEventDialogMoreDetailsButtonText}" />
<Button
Grid.Column="1"
HorizontalAlignment="Stretch"
Command="{x:Bind ViewModel.SaveQuickEventCommand}"
Content="{x:Bind domain:Translator.Buttons_Save}"
Style="{ThemeResource AccentButtonStyle}" />
</Grid>
</Grid>
</Grid>
</Popup>
</Canvas>
</Grid>
</abstract:CalendarPageAbstract>
@@ -1,6 +1,11 @@
using System;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Navigation;
using Windows.Foundation;
using Wino.Calendar.Controls;
using Wino.Calendar.Views.Abstract;
using Wino.Core.Domain.Models.Calendar;
using Wino.Messaging.Client.Calendar;
@@ -9,6 +14,8 @@ namespace Wino.Calendar.Views;
public sealed partial class CalendarPage : CalendarPageAbstract
{
private const int PopupDialogOffset = 12;
public CalendarPage()
{
InitializeComponent();
@@ -38,4 +45,70 @@ public sealed partial class CalendarPage : CalendarPageAbstract
var request = new CalendarDisplayRequest(ViewModel.StatePersistanceService.CalendarDisplayType, anchorDate);
WeakReferenceMessenger.Default.Send(new LoadCalendarMessage(request));
}
private void CalendarSurfaceEmptySlotTapped(object sender, CalendarEmptySlotTappedEventArgs e)
{
if (ViewModel.DisplayDetailsCalendarItemViewModel != null)
{
ViewModel.DisplayDetailsCalendarItemViewModel = null;
return;
}
ViewModel.SelectedQuickEventDate = e.ClickedDate;
var transform = CalendarSurface.TransformToVisual(CalendarOverlayCanvas);
var canvasPoint = transform.TransformPoint(e.PositionerPoint);
TeachingTipPositionerGrid.Width = e.CellSize.Width;
TeachingTipPositionerGrid.Height = e.CellSize.Height;
Canvas.SetLeft(TeachingTipPositionerGrid, canvasPoint.X);
Canvas.SetTop(TeachingTipPositionerGrid, canvasPoint.Y);
var startTime = e.ClickedDate.TimeOfDay;
var endTime = startTime.Add(TimeSpan.FromMinutes(30));
ViewModel.SelectQuickEventTimeRange(startTime, endTime);
QuickEventPopupDialog.IsOpen = true;
}
private void QuickEventAccountSelectorSelectionChanged(object sender, SelectionChangedEventArgs e)
=> QuickEventAccountSelectorFlyout.Hide();
private void QuickEventPopupClosed(object sender, object e)
{
}
private void PopupPlacementChanged(object sender, object e)
{
if (sender is not Popup popup)
{
return;
}
popup.HorizontalOffset = 0;
popup.VerticalOffset = 0;
switch (popup.ActualPlacement)
{
case PopupPlacementMode.Top:
popup.VerticalOffset = PopupDialogOffset * -1;
break;
case PopupPlacementMode.Bottom:
popup.VerticalOffset = PopupDialogOffset;
break;
case PopupPlacementMode.Left:
popup.HorizontalOffset = PopupDialogOffset * -1;
break;
case PopupPlacementMode.Right:
popup.HorizontalOffset = PopupDialogOffset;
break;
}
}
private void StartTimeDurationSubmitted(ComboBox sender, ComboBoxTextSubmittedEventArgs args)
=> ViewModel.SelectedStartTimeString = args.Text;
private void EndTimeDurationSubmitted(ComboBox sender, ComboBoxTextSubmittedEventArgs args)
=> ViewModel.SelectedEndTimeString = args.Text;
}
@@ -175,10 +175,36 @@
<controls:SettingsCard.HeaderIcon>
<PathIcon Data="F1 M 4.921875 18.75 C 4.433594 18.75 3.966471 18.650717 3.520508 18.452148 C 3.074544 18.25358 2.683919 17.986654 2.348633 17.651367 C 2.013346 17.31608 1.746419 16.925455 1.547852 16.479492 C 1.349284 16.033529 1.25 15.566406 1.25 15.078125 L 1.25 4.921875 C 1.25 4.433594 1.349284 3.966473 1.547852 3.520508 C 1.746419 3.074545 2.013346 2.68392 2.348633 2.348633 C 2.683919 2.013348 3.074544 1.74642 3.520508 1.547852 C 3.966471 1.349285 4.433594 1.25 4.921875 1.25 L 15.078125 1.25 C 15.566406 1.25 16.033527 1.349285 16.479492 1.547852 C 16.925455 1.74642 17.31608 2.013348 17.651367 2.348633 C 17.986652 2.68392 18.25358 3.074545 18.452148 3.520508 C 18.650715 3.966473 18.75 4.433594 18.75 4.921875 L 18.75 15.078125 C 18.75 15.566406 18.650715 16.033529 18.452148 16.479492 C 18.25358 16.925455 17.986652 17.31608 17.651367 17.651367 C 17.31608 17.986654 16.925455 18.25358 16.479492 18.452148 C 16.033527 18.650717 15.566406 18.75 15.078125 18.75 Z M 2.5 10 L 17.5 10 L 17.5 4.951172 C 17.5 4.625651 17.433268 4.314779 17.299805 4.018555 C 17.16634 3.722332 16.987305 3.461914 16.762695 3.237305 C 16.538086 3.012695 16.277668 2.83366 15.981445 2.700195 C 15.685221 2.566732 15.374349 2.5 15.048828 2.5 L 4.951172 2.5 C 4.625651 2.5 4.314778 2.566732 4.018555 2.700195 C 3.722331 2.83366 3.461914 3.012695 3.237305 3.237305 C 3.012695 3.461914 2.833659 3.722332 2.700195 4.018555 C 2.566732 4.314779 2.5 4.625651 2.5 4.951172 Z " />
</controls:SettingsCard.HeaderIcon>
<StackPanel Spacing="8">
<ToggleSwitch
IsOn="{x:Bind ViewModel.Is24HourHeaders, Mode=TwoWay}"
OffContent="12h"
OnContent="24h" />
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{x:Bind ViewModel.TimedHourLabelPreview, Mode=OneWay}"
TextWrapping="WrapWholeWords" />
</StackPanel>
</controls:SettingsCard>
<controls:SettingsCard
Description="{x:Bind domain:Translator.CalendarSettings_TimedDayHeaderFormat_Description}"
Header="{x:Bind domain:Translator.CalendarSettings_TimedDayHeaderFormat_Header}">
<controls:SettingsCard.HeaderIcon>
<FontIcon Glyph="&#xE823;" />
</controls:SettingsCard.HeaderIcon>
<StackPanel Spacing="8">
<TextBox
PlaceholderText="ddd dd"
Text="{x:Bind ViewModel.TimedDayHeaderDateFormat, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<ComboBox
ItemsSource="{x:Bind ViewModel.TimedDayHeaderFormatPresets, Mode=OneWay}"
SelectedIndex="{x:Bind ViewModel.SelectedTimedDayHeaderFormatPresetIndex, Mode=TwoWay}" />
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{x:Bind ViewModel.TimedDayHeaderFormatPreview, Mode=OneWay}"
TextWrapping="WrapWholeWords" />
</StackPanel>
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
@@ -289,7 +315,7 @@
<StateTrigger IsActive="{x:Bind ViewModel.Is24HourHeaders, Mode=OneWay}" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="WorkStartStartPicker.ClockIdentifier" Value="24HourClock" />
<Setter Target="WorkHourStartPicker.ClockIdentifier" Value="24HourClock" />
<Setter Target="WorkEndStartPicker.ClockIdentifier" Value="24HourClock" />
</VisualState.Setters>
</VisualState>
+1 -1
View File
@@ -201,7 +201,7 @@ public sealed partial class WinoAppShell : Views.Abstract.WinoAppShellAbstract,
if (ViewModel.IsCalendarMode)
{
ViewModel.StatePersistenceService.CoreWindowTitle = ViewModel.CalendarClient.VisibleDateRangeText ?? string.Empty;
ViewModel.StatePersistenceService.CoreWindowTitle = string.Empty;
return;
}
-4
View File
@@ -120,7 +120,6 @@
<None Remove="Styles\CalendarShellNavigationViewStyle.xaml" />
<None Remove="Styles\CalendarThemeResources.xaml" />
<None Remove="Styles\CalendarViewStyles.xaml" />
<None Remove="Styles\DayHeaderControl.xaml" />
<None Remove="Styles\WinoCalendarTypeSelectorControl.xaml" />
<None Remove="Views\Calendar\CalendarAppShell.xaml" />
<None Remove="Views\Calendar\CalendarPage.xaml" />
@@ -367,9 +366,6 @@
<Page Update="Styles\CalendarThemeResources.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Styles\DayHeaderControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Styles\WinoCalendarTypeSelectorControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff