From 3f2ca490716b88d168441e4d0e821b2cdedc519c Mon Sep 17 00:00:00 2001 From: Patrik Svensson Date: Fri, 16 Oct 2020 16:43:33 +0200 Subject: [PATCH] Add calendar control Closes #101 --- .github/workflows/ci.yaml | 1 + examples/Calendars/Calendars.csproj | 15 + examples/Calendars/Program.cs | 68 +++++ .../Unit/CalendarTests.cs | 92 ++++++ src/Spectre.Console.sln | 15 + .../Extensions/CalendarExtensions.cs | 83 ++++++ .../Extensions/HasCultureExtensions.cs | 73 +++++ src/Spectre.Console/IHasCulture.cs | 15 + .../Internal/Collections/ListWithCallback.cs | 88 ++++++ .../Extensions/DayOfWeekExtensions.cs | 32 ++ src/Spectre.Console/Style.cs | 1 - src/Spectre.Console/Widgets/Calendar.cs | 274 ++++++++++++++++++ src/Spectre.Console/Widgets/CalendarEvent.cs | 54 ++++ src/Spectre.Console/Widgets/Columns.cs | 23 ++ 14 files changed, 833 insertions(+), 1 deletion(-) create mode 100644 examples/Calendars/Calendars.csproj create mode 100644 examples/Calendars/Program.cs create mode 100644 src/Spectre.Console.Tests/Unit/CalendarTests.cs create mode 100644 src/Spectre.Console/Extensions/CalendarExtensions.cs create mode 100644 src/Spectre.Console/Extensions/HasCultureExtensions.cs create mode 100644 src/Spectre.Console/IHasCulture.cs create mode 100644 src/Spectre.Console/Internal/Collections/ListWithCallback.cs create mode 100644 src/Spectre.Console/Internal/Extensions/DayOfWeekExtensions.cs create mode 100644 src/Spectre.Console/Widgets/Calendar.cs create mode 100644 src/Spectre.Console/Widgets/CalendarEvent.cs 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) {