diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index ce2356b..87cccc0 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -71,6 +71,7 @@ jobs:
dotnet example colors
dotnet example emojis
dotnet example exceptions
+ dotnet example calendars
- name: Build
shell: bash
diff --git a/examples/Calendars/Calendars.csproj b/examples/Calendars/Calendars.csproj
new file mode 100644
index 0000000..fbc20a6
--- /dev/null
+++ b/examples/Calendars/Calendars.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ netcoreapp3.1
+ false
+ Calendars
+ Demonstrates how to render calendars.
+
+
+
+
+
+
+
diff --git a/examples/Calendars/Program.cs b/examples/Calendars/Program.cs
new file mode 100644
index 0000000..d5996a6
--- /dev/null
+++ b/examples/Calendars/Program.cs
@@ -0,0 +1,68 @@
+using System.Collections.Generic;
+using Spectre.Console;
+using Spectre.Console.Rendering;
+
+namespace Calendars
+{
+ public static class Program
+ {
+ public static void Main(string[] args)
+ {
+ AnsiConsole.WriteLine();
+ AnsiConsole.Render(
+ new Columns(GetCalendars())
+ .Collapse());
+ }
+
+ private static IEnumerable GetCalendars()
+ {
+ yield return EmbedInPanel(
+ "Invariant calendar",
+ new Calendar(2020, 10)
+ .SimpleHeavyBorder()
+ .SetHighlightStyle(Style.Parse("red"))
+ .AddCalendarEvent("An event", 2020, 9, 22)
+ .AddCalendarEvent("Another event", 2020, 10, 2)
+ .AddCalendarEvent("A third event", 2020, 10, 13));
+
+ yield return EmbedInPanel(
+ "Swedish calendar (sv-SE)",
+ new Calendar(2020, 10)
+ .RoundedBorder()
+ .SetHighlightStyle(Style.Parse("blue"))
+ .SetCulture("sv-SE")
+ .AddCalendarEvent("An event", 2020, 9, 22)
+ .AddCalendarEvent("Another event", 2020, 10, 2)
+ .AddCalendarEvent("A third event", 2020, 10, 13));
+
+ yield return EmbedInPanel(
+ "German calendar (de-DE)",
+ new Calendar(2020, 10)
+ .MarkdownBorder()
+ .SetHighlightStyle(Style.Parse("yellow"))
+ .SetCulture("de-DE")
+ .AddCalendarEvent("An event", 2020, 9, 22)
+ .AddCalendarEvent("Another event", 2020, 10, 2)
+ .AddCalendarEvent("A third event", 2020, 10, 13));
+
+ yield return EmbedInPanel(
+ "Italian calendar (de-DE)",
+ new Calendar(2020, 10)
+ .DoubleBorder()
+ .SetHighlightStyle(Style.Parse("green"))
+ .SetCulture("it-IT")
+ .AddCalendarEvent("An event", 2020, 9, 22)
+ .AddCalendarEvent("Another event", 2020, 10, 2)
+ .AddCalendarEvent("A third event", 2020, 10, 13));
+ }
+
+ private static IRenderable EmbedInPanel(string title, Calendar calendar)
+ {
+ return new Panel(calendar)
+ .Expand()
+ .RoundedBorder()
+ .SetBorderStyle(Style.Parse("grey"))
+ .SetHeader($" {title} ", Style.Parse("yellow"));
+ }
+ }
+}
diff --git a/src/Spectre.Console.Tests/Unit/CalendarTests.cs b/src/Spectre.Console.Tests/Unit/CalendarTests.cs
new file mode 100644
index 0000000..c653523
--- /dev/null
+++ b/src/Spectre.Console.Tests/Unit/CalendarTests.cs
@@ -0,0 +1,92 @@
+using System;
+using Shouldly;
+using Xunit;
+
+namespace Spectre.Console.Tests.Unit
+{
+ public sealed class CalendarTests
+ {
+ [Fact]
+ public void Should_Render_Calendar_Correctly()
+ {
+ // Given
+ var console = new PlainConsole(width: 80);
+ var calendar = new Calendar(2020, 10)
+ .AddCalendarEvent(new DateTime(2020, 9, 1))
+ .AddCalendarEvent(new DateTime(2020, 10, 3))
+ .AddCalendarEvent(new DateTime(2020, 10, 12));
+
+ // When
+ console.Render(calendar);
+
+ // Then
+ console.Lines.Count.ShouldBe(10);
+ console.Lines[0].ShouldBe("┌─────┬─────┬─────┬─────┬─────┬─────┬─────┐");
+ console.Lines[1].ShouldBe("│ Sun │ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │");
+ console.Lines[2].ShouldBe("├─────┼─────┼─────┼─────┼─────┼─────┼─────┤");
+ console.Lines[3].ShouldBe("│ │ │ │ │ 1 │ 2 │ 3* │");
+ console.Lines[4].ShouldBe("│ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │ 10 │");
+ console.Lines[5].ShouldBe("│ 11 │ 12* │ 13 │ 14 │ 15 │ 16 │ 17 │");
+ console.Lines[6].ShouldBe("│ 18 │ 19 │ 20 │ 21 │ 22 │ 23 │ 24 │");
+ console.Lines[7].ShouldBe("│ 25 │ 26 │ 27 │ 28 │ 29 │ 30 │ 31 │");
+ console.Lines[8].ShouldBe("│ │ │ │ │ │ │ │");
+ console.Lines[9].ShouldBe("└─────┴─────┴─────┴─────┴─────┴─────┴─────┘");
+ }
+
+ [Fact]
+ public void Should_Render_Calendar_Correctly_For_Specific_Culture()
+ {
+ // Given
+ var console = new PlainConsole(width: 80);
+ var calendar = new Calendar(2020, 10, 15)
+ .SetCulture("de-DE")
+ .AddCalendarEvent(new DateTime(2020, 9, 1))
+ .AddCalendarEvent(new DateTime(2020, 10, 3))
+ .AddCalendarEvent(new DateTime(2020, 10, 12));
+
+ // When
+ console.Render(calendar);
+
+ // Then
+ console.Lines.Count.ShouldBe(10);
+ console.Lines[0].ShouldBe("┌─────┬────┬────┬────┬────┬────┬────┐");
+ console.Lines[1].ShouldBe("│ Mo │ Di │ Mi │ Do │ Fr │ Sa │ So │");
+ console.Lines[2].ShouldBe("├─────┼────┼────┼────┼────┼────┼────┤");
+ console.Lines[3].ShouldBe("│ │ │ │ 1 │ 2 │ 3* │ 4 │");
+ console.Lines[4].ShouldBe("│ 5 │ 6 │ 7 │ 8 │ 9 │ 10 │ 11 │");
+ console.Lines[5].ShouldBe("│ 12* │ 13 │ 14 │ 15 │ 16 │ 17 │ 18 │");
+ console.Lines[6].ShouldBe("│ 19 │ 20 │ 21 │ 22 │ 23 │ 24 │ 25 │");
+ console.Lines[7].ShouldBe("│ 26 │ 27 │ 28 │ 29 │ 30 │ 31 │ │");
+ console.Lines[8].ShouldBe("│ │ │ │ │ │ │ │");
+ console.Lines[9].ShouldBe("└─────┴────┴────┴────┴────┴────┴────┘");
+ }
+
+ [Fact]
+ public void Should_Render_List_Of_Events_If_Enabled()
+ {
+ // Given
+ var console = new PlainConsole(width: 80);
+ var calendar = new Calendar(2020, 10, 15)
+ .SetCulture("de-DE")
+ .AddCalendarEvent(new DateTime(2020, 9, 1))
+ .AddCalendarEvent(new DateTime(2020, 10, 3))
+ .AddCalendarEvent(new DateTime(2020, 10, 12));
+
+ // When
+ console.Render(calendar);
+
+ // Then
+ console.Lines.Count.ShouldBe(10);
+ console.Lines[0].ShouldBe("┌─────┬────┬────┬────┬────┬────┬────┐");
+ console.Lines[1].ShouldBe("│ Mo │ Di │ Mi │ Do │ Fr │ Sa │ So │");
+ console.Lines[2].ShouldBe("├─────┼────┼────┼────┼────┼────┼────┤");
+ console.Lines[3].ShouldBe("│ │ │ │ 1 │ 2 │ 3* │ 4 │");
+ console.Lines[4].ShouldBe("│ 5 │ 6 │ 7 │ 8 │ 9 │ 10 │ 11 │");
+ console.Lines[5].ShouldBe("│ 12* │ 13 │ 14 │ 15 │ 16 │ 17 │ 18 │");
+ console.Lines[6].ShouldBe("│ 19 │ 20 │ 21 │ 22 │ 23 │ 24 │ 25 │");
+ console.Lines[7].ShouldBe("│ 26 │ 27 │ 28 │ 29 │ 30 │ 31 │ │");
+ console.Lines[8].ShouldBe("│ │ │ │ │ │ │ │");
+ console.Lines[9].ShouldBe("└─────┴────┴────┴────┴────┴────┴────┘");
+ }
+ }
+}
diff --git a/src/Spectre.Console.sln b/src/Spectre.Console.sln
index 9f465ac..50994c1 100644
--- a/src/Spectre.Console.sln
+++ b/src/Spectre.Console.sln
@@ -44,6 +44,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GitHub", "GitHub", "{C3E2CB
..\.github\workflows\publish.yaml = ..\.github\workflows\publish.yaml
EndProjectSection
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Calendars", "..\examples\Calendars\Calendars.csproj", "{57691C7D-683D-46E6-AA4F-57A8C5F65D25}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -198,6 +200,18 @@ Global
{90C081A7-7C1D-4A4A-82B6-8FF473C3EA32}.Release|x64.Build.0 = Release|Any CPU
{90C081A7-7C1D-4A4A-82B6-8FF473C3EA32}.Release|x86.ActiveCfg = Release|Any CPU
{90C081A7-7C1D-4A4A-82B6-8FF473C3EA32}.Release|x86.Build.0 = Release|Any CPU
+ {57691C7D-683D-46E6-AA4F-57A8C5F65D25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {57691C7D-683D-46E6-AA4F-57A8C5F65D25}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {57691C7D-683D-46E6-AA4F-57A8C5F65D25}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {57691C7D-683D-46E6-AA4F-57A8C5F65D25}.Debug|x64.Build.0 = Debug|Any CPU
+ {57691C7D-683D-46E6-AA4F-57A8C5F65D25}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {57691C7D-683D-46E6-AA4F-57A8C5F65D25}.Debug|x86.Build.0 = Debug|Any CPU
+ {57691C7D-683D-46E6-AA4F-57A8C5F65D25}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {57691C7D-683D-46E6-AA4F-57A8C5F65D25}.Release|Any CPU.Build.0 = Release|Any CPU
+ {57691C7D-683D-46E6-AA4F-57A8C5F65D25}.Release|x64.ActiveCfg = Release|Any CPU
+ {57691C7D-683D-46E6-AA4F-57A8C5F65D25}.Release|x64.Build.0 = Release|Any CPU
+ {57691C7D-683D-46E6-AA4F-57A8C5F65D25}.Release|x86.ActiveCfg = Release|Any CPU
+ {57691C7D-683D-46E6-AA4F-57A8C5F65D25}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -214,6 +228,7 @@ Global
{1EABB956-957F-4C1A-8AC0-FD19C8F3C2F2} = {F0575243-121F-4DEE-9F6B-246E26DC0844}
{90C081A7-7C1D-4A4A-82B6-8FF473C3EA32} = {F0575243-121F-4DEE-9F6B-246E26DC0844}
{C3E2CB5C-1517-4C75-B59A-93D4E22BEC8D} = {20595AD4-8D75-4AF8-B6BC-9C38C160423F}
+ {57691C7D-683D-46E6-AA4F-57A8C5F65D25} = {F0575243-121F-4DEE-9F6B-246E26DC0844}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5729B071-67A0-48FB-8B1B-275E6822086C}
diff --git a/src/Spectre.Console/Extensions/CalendarExtensions.cs b/src/Spectre.Console/Extensions/CalendarExtensions.cs
new file mode 100644
index 0000000..92a2988
--- /dev/null
+++ b/src/Spectre.Console/Extensions/CalendarExtensions.cs
@@ -0,0 +1,83 @@
+using System;
+
+namespace Spectre.Console
+{
+ ///
+ /// Contains extension methods for .
+ ///
+ public static class CalendarExtensions
+ {
+ ///
+ /// Adds a calendar event.
+ ///
+ /// The calendar to add the calendar event to.
+ /// The calendar event date.
+ /// The same instance so that multiple calls can be chained.
+ public static Calendar AddCalendarEvent(this Calendar calendar, DateTime date)
+ {
+ return AddCalendarEvent(calendar, string.Empty, date.Year, date.Month, date.Day);
+ }
+
+ ///
+ /// Adds a calendar event.
+ ///
+ /// The calendar to add the calendar event to.
+ /// The calendar event description.
+ /// The calendar event date.
+ /// The same instance so that multiple calls can be chained.
+ public static Calendar AddCalendarEvent(this Calendar calendar, string description, DateTime date)
+ {
+ return AddCalendarEvent(calendar, description, date.Year, date.Month, date.Day);
+ }
+
+ ///
+ /// Adds a calendar event.
+ ///
+ /// The calendar to add the calendar event to.
+ /// The year of the calendar event.
+ /// The month of the calendar event.
+ /// The day of the calendar event.
+ /// The same instance so that multiple calls can be chained.
+ public static Calendar AddCalendarEvent(this Calendar calendar, int year, int month, int day)
+ {
+ return AddCalendarEvent(calendar, string.Empty, year, month, day);
+ }
+
+ ///
+ /// Adds a calendar event.
+ ///
+ /// The calendar.
+ /// The calendar event description.
+ /// The year of the calendar event.
+ /// The month of the calendar event.
+ /// The day of the calendar event.
+ /// The same instance so that multiple calls can be chained.
+ public static Calendar AddCalendarEvent(this Calendar calendar, string description, int year, int month, int day)
+ {
+ if (calendar is null)
+ {
+ throw new ArgumentNullException(nameof(calendar));
+ }
+
+ calendar.CalendarEvents.Add(new CalendarEvent(description, year, month, day));
+ return calendar;
+ }
+
+ ///
+ /// Sets the calendar's highlight .
+ ///
+ /// The calendar.
+ /// The highlight style.
+ /// The same instance so that multiple calls can be chained.
+ public static Calendar SetHighlightStyle(this Calendar calendar, Style? style)
+ {
+ if (calendar is null)
+ {
+ throw new ArgumentNullException(nameof(calendar));
+ }
+
+ calendar.HightlightStyle = style ?? Style.Plain;
+ return calendar;
+ }
+ }
+}
diff --git a/src/Spectre.Console/Extensions/HasCultureExtensions.cs b/src/Spectre.Console/Extensions/HasCultureExtensions.cs
new file mode 100644
index 0000000..45655f5
--- /dev/null
+++ b/src/Spectre.Console/Extensions/HasCultureExtensions.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Globalization;
+
+namespace Spectre.Console
+{
+ ///
+ /// Contains extension methods for .
+ ///
+ public static class HasCultureExtensions
+ {
+ ///
+ /// Sets the culture.
+ ///
+ /// An object type with a culture.
+ /// The object to set the culture for.
+ /// The culture to set.
+ /// The same instance so that multiple calls can be chained.
+ public static T SetCulture(this T obj, CultureInfo culture)
+ where T : class, IHasCulture
+ {
+ if (obj is null)
+ {
+ throw new ArgumentNullException(nameof(obj));
+ }
+
+ if (culture is null)
+ {
+ throw new ArgumentNullException(nameof(culture));
+ }
+
+ obj.Culture = culture;
+ return obj;
+ }
+
+ ///
+ /// Sets the culture.
+ ///
+ /// An object type with a culture.
+ /// The object to set the culture for.
+ /// The culture to set.
+ /// The same instance so that multiple calls can be chained.
+ public static T SetCulture(this T obj, string name)
+ where T : class, IHasCulture
+ {
+ if (obj is null)
+ {
+ throw new ArgumentNullException(nameof(obj));
+ }
+
+ obj.Culture = CultureInfo.GetCultureInfo(name);
+ return obj;
+ }
+
+ ///
+ /// Sets the culture.
+ ///
+ /// An object type with a culture.
+ /// The object to set the culture for.
+ /// The culture to set.
+ /// The same instance so that multiple calls can be chained.
+ public static T SetCulture(this T obj, int name)
+ where T : class, IHasCulture
+ {
+ if (obj is null)
+ {
+ throw new ArgumentNullException(nameof(obj));
+ }
+
+ obj.Culture = CultureInfo.GetCultureInfo(name);
+ return obj;
+ }
+ }
+}
diff --git a/src/Spectre.Console/IHasCulture.cs b/src/Spectre.Console/IHasCulture.cs
new file mode 100644
index 0000000..f1a1073
--- /dev/null
+++ b/src/Spectre.Console/IHasCulture.cs
@@ -0,0 +1,15 @@
+using System.Globalization;
+
+namespace Spectre.Console
+{
+ ///
+ /// Represents something that has a culture.
+ ///
+ public interface IHasCulture
+ {
+ ///
+ /// Gets or sets the culture.
+ ///
+ CultureInfo Culture { get; set; }
+ }
+}
diff --git a/src/Spectre.Console/Internal/Collections/ListWithCallback.cs b/src/Spectre.Console/Internal/Collections/ListWithCallback.cs
new file mode 100644
index 0000000..50dfe41
--- /dev/null
+++ b/src/Spectre.Console/Internal/Collections/ListWithCallback.cs
@@ -0,0 +1,88 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+namespace Spectre.Console.Internal.Collections
+{
+ internal sealed class ListWithCallback : IList
+ {
+ private readonly List _list;
+ private readonly Action _callback;
+
+ public T this[int index]
+ {
+ get => _list[index];
+ set => _list[index] = value;
+ }
+
+ public int Count => _list.Count;
+ public bool IsReadOnly => false;
+
+ public ListWithCallback(Action callback)
+ {
+ _list = new List();
+ _callback = callback ?? throw new ArgumentNullException(nameof(callback));
+ }
+
+ public void Add(T item)
+ {
+ _list.Add(item);
+ _callback();
+ }
+
+ public void Clear()
+ {
+ _list.Clear();
+ _callback();
+ }
+
+ public bool Contains(T item)
+ {
+ return _list.Contains(item);
+ }
+
+ public void CopyTo(T[] array, int arrayIndex)
+ {
+ _list.CopyTo(array, arrayIndex);
+ _callback();
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ return _list.GetEnumerator();
+ }
+
+ public int IndexOf(T item)
+ {
+ return _list.IndexOf(item);
+ }
+
+ public void Insert(int index, T item)
+ {
+ _list.Insert(index, item);
+ _callback();
+ }
+
+ public bool Remove(T item)
+ {
+ var result = _list.Remove(item);
+ if (result)
+ {
+ _callback();
+ }
+
+ return result;
+ }
+
+ public void RemoveAt(int index)
+ {
+ _list.RemoveAt(index);
+ _callback();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+ }
+}
diff --git a/src/Spectre.Console/Internal/Extensions/DayOfWeekExtensions.cs b/src/Spectre.Console/Internal/Extensions/DayOfWeekExtensions.cs
new file mode 100644
index 0000000..df72516
--- /dev/null
+++ b/src/Spectre.Console/Internal/Extensions/DayOfWeekExtensions.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Globalization;
+
+namespace Spectre.Console.Internal
+{
+ internal static class DayOfWeekExtensions
+ {
+ public static string GetAbbreviatedDayName(this DayOfWeek day, CultureInfo culture)
+ {
+ culture ??= CultureInfo.InvariantCulture;
+ var name = culture.DateTimeFormat.GetAbbreviatedDayName(day);
+
+ if (name.Length > 0 && char.IsLower(name[0]))
+ {
+ name = string.Format(culture, "{0}{1}", char.ToUpper(name[0], culture), name.Substring(1));
+ }
+
+ return name;
+ }
+
+ public static DayOfWeek GetNextWeekDay(this DayOfWeek day)
+ {
+ var next = (int)day + 1;
+ if (next > (int)DayOfWeek.Saturday)
+ {
+ return DayOfWeek.Sunday;
+ }
+
+ return (DayOfWeek)next;
+ }
+ }
+}
diff --git a/src/Spectre.Console/Style.cs b/src/Spectre.Console/Style.cs
index b74993e..5b0dbd8 100644
--- a/src/Spectre.Console/Style.cs
+++ b/src/Spectre.Console/Style.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
-using System.Linq;
using Spectre.Console.Internal;
namespace Spectre.Console
diff --git a/src/Spectre.Console/Widgets/Calendar.cs b/src/Spectre.Console/Widgets/Calendar.cs
new file mode 100644
index 0000000..a8412d9
--- /dev/null
+++ b/src/Spectre.Console/Widgets/Calendar.cs
@@ -0,0 +1,274 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using Spectre.Console.Internal;
+using Spectre.Console.Internal.Collections;
+using Spectre.Console.Rendering;
+
+namespace Spectre.Console
+{
+ ///
+ /// A renderable calendar.
+ ///
+ public sealed class Calendar : Renderable, IHasCulture, IHasTableBorder
+ {
+ private const int NumberOfWeekDays = 7;
+ private const int ExpectedRowCount = 6;
+
+ private readonly ListWithCallback _calendarEvents;
+
+ private int _year;
+ private int _month;
+ private int _day;
+ private IRenderable? _table;
+ private TableBorder _border;
+ private bool _useSafeBorder;
+ private Style? _borderStyle;
+ private bool _dirty;
+ private CultureInfo _culture;
+ private Style _highlightStyle;
+
+ ///
+ /// Gets or sets the calendar year.
+ ///
+ public int Year
+ {
+ get => _year;
+ set => MarkAsDirty(() => _year = value);
+ }
+
+ ///
+ /// Gets or sets the calendar month.
+ ///
+ public int Month
+ {
+ get => _month;
+ set => MarkAsDirty(() => _month = value);
+ }
+
+ ///
+ /// Gets or sets the calendar day.
+ ///
+ public int Day
+ {
+ get => _day;
+ set => MarkAsDirty(() => _day = value);
+ }
+
+ ///
+ public TableBorder Border
+ {
+ get => _border;
+ set => MarkAsDirty(() => _border = value);
+ }
+
+ ///
+ public bool UseSafeBorder
+ {
+ get => _useSafeBorder;
+ set => MarkAsDirty(() => _useSafeBorder = value);
+ }
+
+ ///
+ public Style? BorderStyle
+ {
+ get => _borderStyle;
+ set => MarkAsDirty(() => _borderStyle = value);
+ }
+
+ ///
+ /// Gets or sets the calendar's .
+ ///
+ public CultureInfo Culture
+ {
+ get => _culture;
+ set => MarkAsDirty(() => _culture = value);
+ }
+
+ ///
+ /// Gets or sets the calendar's highlight .
+ ///
+ public Style HightlightStyle
+ {
+ get => _highlightStyle;
+ set => MarkAsDirty(() => _highlightStyle = value);
+ }
+
+ ///
+ /// Gets a list containing all calendar events.
+ ///
+ public IList CalendarEvents => _calendarEvents;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The calendar date.
+ public Calendar(DateTime date)
+ : this(date.Year, date.Month, date.Day)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The calendar year.
+ /// The calendar month.
+ public Calendar(int year, int month)
+ : this(year, month, 1)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The calendar year.
+ /// The calendar month.
+ /// The calendar day.
+ public Calendar(int year, int month, int day)
+ {
+ _year = year;
+ _month = month;
+ _day = day;
+ _table = null;
+ _border = TableBorder.Square;
+ _useSafeBorder = true;
+ _borderStyle = null;
+ _dirty = true;
+ _culture = CultureInfo.InvariantCulture;
+ _highlightStyle = new Style(foreground: Color.Blue);
+ _calendarEvents = new ListWithCallback(() => _dirty = true);
+ }
+
+ ///
+ protected override Measurement Measure(RenderContext context, int maxWidth)
+ {
+ var table = GetTable();
+ return table.Measure(context, maxWidth);
+ }
+
+ ///
+ protected override IEnumerable Render(RenderContext context, int maxWidth)
+ {
+ return GetTable().Render(context, maxWidth);
+ }
+
+ private IRenderable GetTable()
+ {
+ // Table needs to be built?
+ if (_dirty || _table == null)
+ {
+ _table = BuildTable();
+ _dirty = false;
+ }
+
+ return _table;
+ }
+
+ private IRenderable BuildTable()
+ {
+ var culture = Culture ?? CultureInfo.InvariantCulture;
+
+ var table = new Table
+ {
+ Border = _border,
+ UseSafeBorder = _useSafeBorder,
+ BorderStyle = _borderStyle,
+ };
+
+ // Add columns
+ foreach (var order in GetWeekdays())
+ {
+ table.AddColumn(new TableColumn(order.GetAbbreviatedDayName(culture)));
+ }
+
+ var row = new List();
+
+ var currentDay = 1;
+ var weekday = culture.DateTimeFormat.FirstDayOfWeek;
+ var weekdays = BuildWeekDayTable();
+
+ var daysInMonth = DateTime.DaysInMonth(Year, Month);
+ while (currentDay <= daysInMonth)
+ {
+ if (weekdays[currentDay - 1] == weekday)
+ {
+ if (_calendarEvents.Any(e => e.Month == Month && e.Day == currentDay))
+ {
+ row.Add(new Markup(currentDay.ToString(CultureInfo.InvariantCulture) + "*", _highlightStyle));
+ }
+ else
+ {
+ row.Add(new Text(currentDay.ToString(CultureInfo.InvariantCulture)));
+ }
+
+ currentDay++;
+ }
+ else
+ {
+ // Add empty cell
+ row.Add(Text.Empty);
+ }
+
+ if (row.Count == NumberOfWeekDays)
+ {
+ // Flush row
+ table.AddRow(row.ToArray());
+ row.Clear();
+ }
+
+ weekday = weekday.GetNextWeekDay();
+ }
+
+ if (row.Count > 0)
+ {
+ // Flush row
+ table.AddRow(row.ToArray());
+ row.Clear();
+ }
+
+ // We want all calendars to have the same height.
+ if (table.RowCount < ExpectedRowCount)
+ {
+ var diff = Math.Max(0, ExpectedRowCount - table.RowCount);
+ for (var i = 0; i < diff; i++)
+ {
+ table.AddEmptyRow();
+ }
+ }
+
+ return table;
+ }
+
+ private void MarkAsDirty(Action action)
+ {
+ action();
+ _dirty = true;
+ }
+
+ private DayOfWeek[] GetWeekdays()
+ {
+ var culture = Culture ?? CultureInfo.InvariantCulture;
+
+ var days = new DayOfWeek[7];
+ days[0] = culture.DateTimeFormat.FirstDayOfWeek;
+ for (var i = 1; i < 7; i++)
+ {
+ days[i] = days[i - 1].GetNextWeekDay();
+ }
+
+ return days;
+ }
+
+ private DayOfWeek[] BuildWeekDayTable()
+ {
+ var result = new List();
+ for (var day = 0; day < DateTime.DaysInMonth(Year, Month); day++)
+ {
+ result.Add(new DateTime(Year, Month, day + 1).DayOfWeek);
+ }
+
+ return result.ToArray();
+ }
+ }
+}
diff --git a/src/Spectre.Console/Widgets/CalendarEvent.cs b/src/Spectre.Console/Widgets/CalendarEvent.cs
new file mode 100644
index 0000000..325ba20
--- /dev/null
+++ b/src/Spectre.Console/Widgets/CalendarEvent.cs
@@ -0,0 +1,54 @@
+namespace Spectre.Console
+{
+ ///
+ /// Represents a calendar event.
+ ///
+ public sealed class CalendarEvent
+ {
+ ///
+ /// Gets the description of the calendar event.
+ ///
+ public string Description { get; }
+
+ ///
+ /// Gets the year of the calendar event.
+ ///
+ public int Year { get; }
+
+ ///
+ /// Gets the month of the calendar event.
+ ///
+ public int Month { get; }
+
+ ///
+ /// Gets the day of the calendar event.
+ ///
+ public int Day { get; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The year of the calendar event.
+ /// The month of the calendar event.
+ /// The day of the calendar event.
+ public CalendarEvent(int year, int month, int day)
+ : this(string.Empty, year, month, day)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The calendar event description.
+ /// The year of the calendar event.
+ /// The month of the calendar event.
+ /// The day of the calendar event.
+ public CalendarEvent(string description, int year, int month, int day)
+ {
+ Description = description ?? string.Empty;
+ Year = year;
+ Month = month;
+ Day = day;
+ }
+ }
+}
diff --git a/src/Spectre.Console/Widgets/Columns.cs b/src/Spectre.Console/Widgets/Columns.cs
index 2d10968..55c287c 100644
--- a/src/Spectre.Console/Widgets/Columns.cs
+++ b/src/Spectre.Console/Widgets/Columns.cs
@@ -50,6 +50,29 @@ namespace Spectre.Console
_items = new List(items.Select(item => new Markup(item)));
}
+ ///
+ protected override Measurement Measure(RenderContext context, int maxWidth)
+ {
+ var maxPadding = Math.Max(Padding.Left, Padding.Right);
+
+ var itemWidths = _items.Select(item => item.Measure(context, maxWidth).Max).ToArray();
+ var columnCount = CalculateColumnCount(maxWidth, itemWidths, _items.Count, maxPadding);
+
+ var rows = _items.Count / columnCount;
+ var greatestWidth = 0;
+ for (var row = 0; row < rows; row += columnCount)
+ {
+ var widths = itemWidths.Skip(row * columnCount).Take(columnCount).ToList();
+ var totalWidth = widths.Sum() + (maxPadding * (widths.Count - 1));
+ if (totalWidth > greatestWidth)
+ {
+ greatestWidth = totalWidth;
+ }
+ }
+
+ return new Measurement(greatestWidth, greatestWidth);
+ }
+
///
protected override IEnumerable Render(RenderContext context, int maxWidth)
{