This commit is contained in:
coco
2026-07-16 09:43:06 +08:00
parent 95aa018b8b
commit 63bb06ff63
95 changed files with 6004 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
/build
+256
View File
@@ -0,0 +1,256 @@
public final class io/github/boguszpawlowski/composecalendar/BuildConfig {
public static final field BUILD_TYPE Ljava/lang/String;
public static final field DEBUG Z
public static final field LIBRARY_PACKAGE_NAME Ljava/lang/String;
public fun <init> ()V
}
public final class io/github/boguszpawlowski/composecalendar/CalendarKt {
public static final fun Calendar (Lio/github/boguszpawlowski/composecalendar/CalendarState;Landroidx/compose/ui/Modifier;Ljava/time/DayOfWeek;Ljava/time/LocalDate;ZZZLkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V
public static final fun SelectableCalendar (Landroidx/compose/ui/Modifier;Ljava/time/DayOfWeek;Ljava/time/LocalDate;ZZZLio/github/boguszpawlowski/composecalendar/CalendarState;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V
public static final fun StaticCalendar (Landroidx/compose/ui/Modifier;Ljava/time/DayOfWeek;Ljava/time/LocalDate;ZZZLio/github/boguszpawlowski/composecalendar/CalendarState;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V
public static final fun rememberCalendarState (Ljava/time/YearMonth;Ljava/time/YearMonth;Ljava/time/YearMonth;Lio/github/boguszpawlowski/composecalendar/header/MonthState;Landroidx/compose/runtime/Composer;II)Lio/github/boguszpawlowski/composecalendar/CalendarState;
public static final fun rememberSelectableCalendarState (Ljava/time/YearMonth;Ljava/time/YearMonth;Ljava/time/YearMonth;Ljava/util/List;Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;Lkotlin/jvm/functions/Function1;Lio/github/boguszpawlowski/composecalendar/header/MonthState;Lio/github/boguszpawlowski/composecalendar/selection/DynamicSelectionState;Landroidx/compose/runtime/Composer;II)Lio/github/boguszpawlowski/composecalendar/CalendarState;
}
public final class io/github/boguszpawlowski/composecalendar/CalendarState {
public static final field $stable I
public fun <init> (Lio/github/boguszpawlowski/composecalendar/header/MonthState;Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;)V
public final fun getMonthState ()Lio/github/boguszpawlowski/composecalendar/header/MonthState;
public final fun getSelectionState ()Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;
}
public final class io/github/boguszpawlowski/composecalendar/ComposableSingletons$CalendarKt {
public static final field INSTANCE Lio/github/boguszpawlowski/composecalendar/ComposableSingletons$CalendarKt;
public static field lambda-1 Lkotlin/jvm/functions/Function4;
public static field lambda-10 Lkotlin/jvm/functions/Function4;
public static field lambda-11 Lkotlin/jvm/functions/Function4;
public static field lambda-12 Lkotlin/jvm/functions/Function3;
public static field lambda-2 Lkotlin/jvm/functions/Function4;
public static field lambda-3 Lkotlin/jvm/functions/Function4;
public static field lambda-4 Lkotlin/jvm/functions/Function3;
public static field lambda-5 Lkotlin/jvm/functions/Function4;
public static field lambda-6 Lkotlin/jvm/functions/Function4;
public static field lambda-7 Lkotlin/jvm/functions/Function4;
public static field lambda-8 Lkotlin/jvm/functions/Function3;
public static field lambda-9 Lkotlin/jvm/functions/Function4;
public fun <init> ()V
public final fun getLambda-1$library_release ()Lkotlin/jvm/functions/Function4;
public final fun getLambda-10$library_release ()Lkotlin/jvm/functions/Function4;
public final fun getLambda-11$library_release ()Lkotlin/jvm/functions/Function4;
public final fun getLambda-12$library_release ()Lkotlin/jvm/functions/Function3;
public final fun getLambda-2$library_release ()Lkotlin/jvm/functions/Function4;
public final fun getLambda-3$library_release ()Lkotlin/jvm/functions/Function4;
public final fun getLambda-4$library_release ()Lkotlin/jvm/functions/Function3;
public final fun getLambda-5$library_release ()Lkotlin/jvm/functions/Function4;
public final fun getLambda-6$library_release ()Lkotlin/jvm/functions/Function4;
public final fun getLambda-7$library_release ()Lkotlin/jvm/functions/Function4;
public final fun getLambda-8$library_release ()Lkotlin/jvm/functions/Function3;
public final fun getLambda-9$library_release ()Lkotlin/jvm/functions/Function4;
}
public final class io/github/boguszpawlowski/composecalendar/ComposableSingletons$WeekCalendarKt {
public static final field INSTANCE Lio/github/boguszpawlowski/composecalendar/ComposableSingletons$WeekCalendarKt;
public static field lambda-1 Lkotlin/jvm/functions/Function4;
public static field lambda-2 Lkotlin/jvm/functions/Function4;
public static field lambda-3 Lkotlin/jvm/functions/Function4;
public static field lambda-4 Lkotlin/jvm/functions/Function4;
public static field lambda-5 Lkotlin/jvm/functions/Function4;
public static field lambda-6 Lkotlin/jvm/functions/Function4;
public static field lambda-7 Lkotlin/jvm/functions/Function4;
public static field lambda-8 Lkotlin/jvm/functions/Function4;
public static field lambda-9 Lkotlin/jvm/functions/Function4;
public fun <init> ()V
public final fun getLambda-1$library_release ()Lkotlin/jvm/functions/Function4;
public final fun getLambda-2$library_release ()Lkotlin/jvm/functions/Function4;
public final fun getLambda-3$library_release ()Lkotlin/jvm/functions/Function4;
public final fun getLambda-4$library_release ()Lkotlin/jvm/functions/Function4;
public final fun getLambda-5$library_release ()Lkotlin/jvm/functions/Function4;
public final fun getLambda-6$library_release ()Lkotlin/jvm/functions/Function4;
public final fun getLambda-7$library_release ()Lkotlin/jvm/functions/Function4;
public final fun getLambda-8$library_release ()Lkotlin/jvm/functions/Function4;
public final fun getLambda-9$library_release ()Lkotlin/jvm/functions/Function4;
}
public final class io/github/boguszpawlowski/composecalendar/WeekCalendarKt {
public static final fun SelectableWeekCalendar (Landroidx/compose/ui/Modifier;Ljava/time/DayOfWeek;Ljava/time/LocalDate;ZZLio/github/boguszpawlowski/composecalendar/WeekCalendarState;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;II)V
public static final fun StaticWeekCalendar (Landroidx/compose/ui/Modifier;Ljava/time/DayOfWeek;Ljava/time/LocalDate;ZLio/github/boguszpawlowski/composecalendar/WeekCalendarState;ZLkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;II)V
public static final fun WeekCalendar (Lio/github/boguszpawlowski/composecalendar/WeekCalendarState;Landroidx/compose/ui/Modifier;Ljava/time/LocalDate;Ljava/time/DayOfWeek;ZZLkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;II)V
public static final fun rememberSelectableWeekCalendarState (Ljava/time/DayOfWeek;Lio/github/boguszpawlowski/composecalendar/week/Week;Lio/github/boguszpawlowski/composecalendar/week/Week;Lio/github/boguszpawlowski/composecalendar/week/Week;Ljava/util/List;Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;Lkotlin/jvm/functions/Function1;Lio/github/boguszpawlowski/composecalendar/header/WeekState;Lio/github/boguszpawlowski/composecalendar/selection/DynamicSelectionState;Landroidx/compose/runtime/Composer;II)Lio/github/boguszpawlowski/composecalendar/WeekCalendarState;
public static final fun rememberWeekCalendarState (Ljava/time/DayOfWeek;Lio/github/boguszpawlowski/composecalendar/week/Week;Lio/github/boguszpawlowski/composecalendar/week/Week;Lio/github/boguszpawlowski/composecalendar/week/Week;Lio/github/boguszpawlowski/composecalendar/header/WeekState;Landroidx/compose/runtime/Composer;II)Lio/github/boguszpawlowski/composecalendar/WeekCalendarState;
}
public final class io/github/boguszpawlowski/composecalendar/WeekCalendarState {
public static final field $stable I
public fun <init> (Lio/github/boguszpawlowski/composecalendar/header/WeekState;Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;)V
public final fun getSelectionState ()Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;
public final fun getWeekState ()Lio/github/boguszpawlowski/composecalendar/header/WeekState;
}
public abstract interface class io/github/boguszpawlowski/composecalendar/day/Day {
public abstract fun getDate ()Ljava/time/LocalDate;
public abstract fun isCurrentDay ()Z
public abstract fun isFromCurrentMonth ()Z
}
public final class io/github/boguszpawlowski/composecalendar/day/DayState : io/github/boguszpawlowski/composecalendar/day/Day {
public static final field $stable I
public fun <init> (Lio/github/boguszpawlowski/composecalendar/day/Day;Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;)V
public final fun component2 ()Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;
public final fun copy (Lio/github/boguszpawlowski/composecalendar/day/Day;Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;)Lio/github/boguszpawlowski/composecalendar/day/DayState;
public static synthetic fun copy$default (Lio/github/boguszpawlowski/composecalendar/day/DayState;Lio/github/boguszpawlowski/composecalendar/day/Day;Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;ILjava/lang/Object;)Lio/github/boguszpawlowski/composecalendar/day/DayState;
public fun equals (Ljava/lang/Object;)Z
public fun getDate ()Ljava/time/LocalDate;
public final fun getSelectionState ()Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;
public fun hashCode ()I
public fun isCurrentDay ()Z
public fun isFromCurrentMonth ()Z
public fun toString ()Ljava/lang/String;
}
public final class io/github/boguszpawlowski/composecalendar/day/DefaultDayKt {
public static final fun DefaultDay-t6yy7ic (Lio/github/boguszpawlowski/composecalendar/day/DayState;Landroidx/compose/ui/Modifier;JJLkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V
}
public final class io/github/boguszpawlowski/composecalendar/header/ComposableSingletons$DefaultMonthHeaderKt {
public static final field INSTANCE Lio/github/boguszpawlowski/composecalendar/header/ComposableSingletons$DefaultMonthHeaderKt;
public static field lambda-1 Lkotlin/jvm/functions/Function2;
public static field lambda-2 Lkotlin/jvm/functions/Function2;
public fun <init> ()V
public final fun getLambda-1$library_release ()Lkotlin/jvm/functions/Function2;
public final fun getLambda-2$library_release ()Lkotlin/jvm/functions/Function2;
}
public final class io/github/boguszpawlowski/composecalendar/header/ComposableSingletons$DefaultWeekHeaderKt {
public static final field INSTANCE Lio/github/boguszpawlowski/composecalendar/header/ComposableSingletons$DefaultWeekHeaderKt;
public static field lambda-1 Lkotlin/jvm/functions/Function2;
public static field lambda-2 Lkotlin/jvm/functions/Function2;
public fun <init> ()V
public final fun getLambda-1$library_release ()Lkotlin/jvm/functions/Function2;
public final fun getLambda-2$library_release ()Lkotlin/jvm/functions/Function2;
}
public final class io/github/boguszpawlowski/composecalendar/header/DefaultMonthHeaderKt {
public static final fun DefaultMonthHeader (Lio/github/boguszpawlowski/composecalendar/header/MonthState;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V
}
public final class io/github/boguszpawlowski/composecalendar/header/DefaultWeekHeaderKt {
public static final fun DefaultWeekHeader (Lio/github/boguszpawlowski/composecalendar/header/WeekState;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V
}
public abstract interface class io/github/boguszpawlowski/composecalendar/header/MonthState {
public static final field Companion Lio/github/boguszpawlowski/composecalendar/header/MonthState$Companion;
public abstract fun getCurrentMonth ()Ljava/time/YearMonth;
public abstract fun getMaxMonth ()Ljava/time/YearMonth;
public abstract fun getMinMonth ()Ljava/time/YearMonth;
public abstract fun setCurrentMonth (Ljava/time/YearMonth;)V
public abstract fun setMaxMonth (Ljava/time/YearMonth;)V
public abstract fun setMinMonth (Ljava/time/YearMonth;)V
}
public final class io/github/boguszpawlowski/composecalendar/header/MonthState$Companion {
public final fun Saver ()Landroidx/compose/runtime/saveable/Saver;
}
public final class io/github/boguszpawlowski/composecalendar/header/MonthStateKt {
public static final fun MonthState (Ljava/time/YearMonth;Ljava/time/YearMonth;Ljava/time/YearMonth;)Lio/github/boguszpawlowski/composecalendar/header/MonthState;
}
public abstract interface class io/github/boguszpawlowski/composecalendar/header/WeekState {
public static final field Companion Lio/github/boguszpawlowski/composecalendar/header/WeekState$Companion;
public abstract fun getCurrentWeek ()Lio/github/boguszpawlowski/composecalendar/week/Week;
public abstract fun getMaxWeek ()Lio/github/boguszpawlowski/composecalendar/week/Week;
public abstract fun getMinWeek ()Lio/github/boguszpawlowski/composecalendar/week/Week;
public abstract fun setCurrentWeek (Lio/github/boguszpawlowski/composecalendar/week/Week;)V
public abstract fun setMaxWeek (Lio/github/boguszpawlowski/composecalendar/week/Week;)V
public abstract fun setMinWeek (Lio/github/boguszpawlowski/composecalendar/week/Week;)V
}
public final class io/github/boguszpawlowski/composecalendar/header/WeekState$Companion {
public final fun Saver ()Landroidx/compose/runtime/saveable/Saver;
}
public final class io/github/boguszpawlowski/composecalendar/header/WeekStateKt {
public static final fun WeekState (Lio/github/boguszpawlowski/composecalendar/week/Week;Lio/github/boguszpawlowski/composecalendar/week/Week;Lio/github/boguszpawlowski/composecalendar/week/Week;)Lio/github/boguszpawlowski/composecalendar/header/WeekState;
}
public final class io/github/boguszpawlowski/composecalendar/selection/DynamicSelectionHandler {
public static final field $stable I
public static final field INSTANCE Lio/github/boguszpawlowski/composecalendar/selection/DynamicSelectionHandler;
public final fun calculateNewSelection (Ljava/time/LocalDate;Ljava/util/List;Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;)Ljava/util/List;
}
public final class io/github/boguszpawlowski/composecalendar/selection/DynamicSelectionState : io/github/boguszpawlowski/composecalendar/selection/SelectionState {
public static final field $stable I
public fun <init> (Lkotlin/jvm/functions/Function1;Ljava/util/List;Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;)V
public synthetic fun <init> (Lkotlin/jvm/functions/Function1;Ljava/util/List;Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getSelection ()Ljava/util/List;
public final fun getSelectionMode ()Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;
public fun isDateSelected (Ljava/time/LocalDate;)Z
public fun onDateSelected (Ljava/time/LocalDate;)V
public final fun setSelection (Ljava/util/List;)V
public final fun setSelectionMode (Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;)V
}
public final class io/github/boguszpawlowski/composecalendar/selection/EmptySelectionState : io/github/boguszpawlowski/composecalendar/selection/SelectionState {
public static final field $stable I
public static final field INSTANCE Lio/github/boguszpawlowski/composecalendar/selection/EmptySelectionState;
public fun isDateSelected (Ljava/time/LocalDate;)Z
public fun onDateSelected (Ljava/time/LocalDate;)V
}
public final class io/github/boguszpawlowski/composecalendar/selection/SelectionMode : java/lang/Enum {
public static final field Multiple Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;
public static final field None Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;
public static final field Period Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;
public static final field Single Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;
public static fun values ()[Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;
}
public abstract interface class io/github/boguszpawlowski/composecalendar/selection/SelectionState {
public abstract fun isDateSelected (Ljava/time/LocalDate;)Z
public abstract fun onDateSelected (Ljava/time/LocalDate;)V
}
public final class io/github/boguszpawlowski/composecalendar/selection/SelectionState$DefaultImpls {
public static fun isDateSelected (Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;Ljava/time/LocalDate;)Z
public static fun onDateSelected (Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;Ljava/time/LocalDate;)V
}
public final class io/github/boguszpawlowski/composecalendar/week/DefaultWeekHeaderKt {
public static final fun DefaultDaysOfWeekHeader (Ljava/util/List;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V
public static final fun DefaultWeekHeader (Ljava/util/List;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V
}
public final class io/github/boguszpawlowski/composecalendar/week/Week {
public static final field $stable I
public static final field Companion Lio/github/boguszpawlowski/composecalendar/week/Week$Companion;
public fun <init> (Ljava/util/List;)V
public final fun compareTo (Lio/github/boguszpawlowski/composecalendar/week/Week;)I
public final fun component1 ()Ljava/util/List;
public final fun copy (Ljava/util/List;)Lio/github/boguszpawlowski/composecalendar/week/Week;
public static synthetic fun copy$default (Lio/github/boguszpawlowski/composecalendar/week/Week;Ljava/util/List;ILjava/lang/Object;)Lio/github/boguszpawlowski/composecalendar/week/Week;
public final fun dec ()Lio/github/boguszpawlowski/composecalendar/week/Week;
public fun equals (Ljava/lang/Object;)Z
public final fun getDays ()Ljava/util/List;
public final fun getEnd ()Ljava/time/LocalDate;
public final fun getStart ()Ljava/time/LocalDate;
public final fun getYearMonth ()Ljava/time/YearMonth;
public fun hashCode ()I
public final fun inc ()Lio/github/boguszpawlowski/composecalendar/week/Week;
public final fun minusWeeks (J)Lio/github/boguszpawlowski/composecalendar/week/Week;
public final fun plusWeeks (J)Lio/github/boguszpawlowski/composecalendar/week/Week;
public fun toString ()Ljava/lang/String;
}
public final class io/github/boguszpawlowski/composecalendar/week/Week$Companion {
public final fun now (Ljava/time/DayOfWeek;)Lio/github/boguszpawlowski/composecalendar/week/Week;
public static synthetic fun now$default (Lio/github/boguszpawlowski/composecalendar/week/Week$Companion;Ljava/time/DayOfWeek;ILjava/lang/Object;)Lio/github/boguszpawlowski/composecalendar/week/Week;
}
public final class io/github/boguszpawlowski/composecalendar/week/WeekKt {
public static final fun between (Ljava/time/temporal/ChronoUnit;Lio/github/boguszpawlowski/composecalendar/week/Week;Lio/github/boguszpawlowski/composecalendar/week/Week;)I
}
+30
View File
@@ -0,0 +1,30 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id(Android.LibraryPluginId)
kotlin(Kotlin.AndroidPluginId)
id("common-android-plugin")
id(MavenPublish.PluginId)
}
android {
namespace = "io.github.boguszpawlowski.composecalendar"
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = freeCompilerArgs + "-Xexplicit-api=strict" + "-Xcontext-receivers"
}
}
}
dependencies {
implementation(Compose.Ui)
implementation(Compose.UiTooling)
implementation(Compose.AccompanistPager)
implementation(Compose.Foundation)
implementation(Compose.FoundationLayout)
implementation(Compose.Material)
implementation(Timber.Core)
testImplementation(Kotest.Assertions)
testImplementation(Kotest.RunnerJunit5)
}
@@ -0,0 +1,5 @@
POM_ARTIFACT_ID=composecalendar
POM_DESCRIPTION=Library for handling the Calendar view in Jetpack Compose.
POM_PACKAGING=aar
SONATYPE_HOST=S01
RELEASE_SIGNING_ENABLED=true
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
@@ -0,0 +1,286 @@
@file:Suppress("MatchingDeclarationName")
package io.github.boguszpawlowski.composecalendar
import android.annotation.SuppressLint
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import io.github.boguszpawlowski.composecalendar.day.DayState
import io.github.boguszpawlowski.composecalendar.day.DefaultDay
import io.github.boguszpawlowski.composecalendar.header.DefaultMonthHeader
import io.github.boguszpawlowski.composecalendar.header.MonthState
import io.github.boguszpawlowski.composecalendar.month.MonthContent
import io.github.boguszpawlowski.composecalendar.month.MonthPager
import io.github.boguszpawlowski.composecalendar.selection.DynamicSelectionState
import io.github.boguszpawlowski.composecalendar.selection.EmptySelectionState
import io.github.boguszpawlowski.composecalendar.selection.SelectionMode
import io.github.boguszpawlowski.composecalendar.selection.SelectionState
import io.github.boguszpawlowski.composecalendar.week.DaysInAWeek
import io.github.boguszpawlowski.composecalendar.week.DefaultDaysOfWeekHeader
import io.github.boguszpawlowski.composecalendar.week.rotateRight
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.YearMonth
import java.time.temporal.WeekFields
import java.util.Locale
/**
* State of the calendar composable
*
* @property monthState currently showed month
* @property selectionState handler for the calendar's selection
*/
@Stable
public class CalendarState<T : SelectionState>(
public val monthState: MonthState,
public val selectionState: T,
)
/**
* [Calendar] implementation using a [DynamicSelectionState] as a selection handler.
*
* * Basic usage:
* ```
* @Composable
* fun MainScreen() {
* SelectableCalendar()
* }
* ```
*
* @param modifier
* @param firstDayOfWeek first day of a week, defaults to current locale's
* @param today current day, defaults to [LocalDate.now]
* @param showAdjacentMonths whenever to show or hide the days from adjacent months
* @param horizontalSwipeEnabled whenever user is able to change the month by horizontal swipe
* @param weekDaysScrollEnabled if the week days should be scrollable
* @param calendarState state of the composable
* @param dayContent composable rendering the current day
* @param monthHeader header for showing the current month and controls for changing it
* @param daysOfWeekHeader header for showing captions for each day of week
* @param monthContainer container composable for all the days in current month
*/
@Composable
@SuppressLint("NewApi")
public fun SelectableCalendar(
modifier: Modifier = Modifier,
firstDayOfWeek: DayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek,
today: LocalDate = LocalDate.now(),
showAdjacentMonths: Boolean = true,
horizontalSwipeEnabled: Boolean = true,
weekDaysScrollEnabled: Boolean = true,
calendarState: CalendarState<DynamicSelectionState> = rememberSelectableCalendarState(),
dayContent: @Composable BoxScope.(DayState<DynamicSelectionState>) -> Unit = { DefaultDay(it) },
monthHeader: @Composable ColumnScope.(MonthState) -> Unit = { DefaultMonthHeader(it) },
daysOfWeekHeader: @Composable BoxScope.(List<DayOfWeek>) -> Unit = { DefaultDaysOfWeekHeader(it) },
monthContainer: @Composable (content: @Composable (PaddingValues) -> Unit) -> Unit = { content ->
Box { content(PaddingValues()) }
},
) {
Calendar(
modifier = modifier,
firstDayOfWeek = firstDayOfWeek,
today = today,
showAdjacentMonths = showAdjacentMonths,
horizontalSwipeEnabled = horizontalSwipeEnabled,
weekDaysScrollEnabled = weekDaysScrollEnabled,
calendarState = calendarState,
dayContent = dayContent,
monthHeader = monthHeader,
daysOfWeekHeader = daysOfWeekHeader,
monthContainer = monthContainer
)
}
/**
* [Calendar] implementation without any mechanism for the selection.
*
* Basic usage:
* ```
* @Composable
* fun MainScreen() {
* StaticCalendar()
* }
* ```
*
* @param modifier
* @param firstDayOfWeek first day of a week, defaults to current locale's
* @param today current day, defaults to [LocalDate.now]
* @param showAdjacentMonths whenever to show or hide the days from adjacent months
* @param horizontalSwipeEnabled whenever user is able to change the month by horizontal swipe
* @param weekDaysScrollEnabled if the week days should be scrollable
* @param calendarState state of the composable
* @param dayContent composable rendering the current day
* @param monthHeader header for showing the current month and controls for changing it
* @param daysOfWeekHeader header for showing captions for each day of week
* @param monthContainer container composable for all the days in current month
*/
@Composable
@SuppressLint("NewApi")
public fun StaticCalendar(
modifier: Modifier = Modifier,
firstDayOfWeek: DayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek,
today: LocalDate = LocalDate.now(),
showAdjacentMonths: Boolean = true,
horizontalSwipeEnabled: Boolean = true,
weekDaysScrollEnabled: Boolean = true,
calendarState: CalendarState<EmptySelectionState> = rememberCalendarState(),
dayContent: @Composable BoxScope.(DayState<EmptySelectionState>) -> Unit = { DefaultDay(it) },
monthHeader: @Composable ColumnScope.(MonthState) -> Unit = { DefaultMonthHeader(it) },
daysOfWeekHeader: @Composable BoxScope.(List<DayOfWeek>) -> Unit = { DefaultDaysOfWeekHeader(it) },
monthContainer: @Composable (content: @Composable (PaddingValues) -> Unit) -> Unit = { content ->
Box { content(PaddingValues()) }
},
) {
Calendar(
modifier = modifier,
firstDayOfWeek = firstDayOfWeek,
today = today,
showAdjacentMonths = showAdjacentMonths,
horizontalSwipeEnabled = horizontalSwipeEnabled,
weekDaysScrollEnabled = weekDaysScrollEnabled,
calendarState = calendarState,
dayContent = dayContent,
monthHeader = monthHeader,
daysOfWeekHeader = daysOfWeekHeader,
monthContainer = monthContainer
)
}
/**
* Composable for showing a calendar. The calendar state has to be provided by the call site. If you
* want to use built-in implementation, check out:
* [SelectableCalendar] - calendar composable handling selection that can be changed at runtime
* [StaticCalendar] - calendar without any mechanism for selection
*
* @param modifier
* @param firstDayOfWeek first day of a week, defaults to current locale's
* @param today current day, defaults to [LocalDate.now]
* @param showAdjacentMonths whenever to show or hide the days from adjacent months
* @param horizontalSwipeEnabled whenever user is able to change the month by horizontal swipe
* @param calendarState state of the composable
* @param dayContent composable rendering the current day
* @param monthHeader header for showing the current month and controls for changing it
* @param daysOfWeekHeader header for showing captions for each day of week
* @param monthContainer container composable for all the days in current month
*/
@Composable
@SuppressLint("NewApi")
public fun <T : SelectionState> Calendar(
calendarState: CalendarState<T>,
modifier: Modifier = Modifier,
firstDayOfWeek: DayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek,
today: LocalDate = LocalDate.now(),
showAdjacentMonths: Boolean = true,
horizontalSwipeEnabled: Boolean = true,
weekDaysScrollEnabled: Boolean = true,
dayContent: @Composable BoxScope.(DayState<T>) -> Unit = { DefaultDay(it) },
monthHeader: @Composable ColumnScope.(MonthState) -> Unit = { DefaultMonthHeader(it) },
daysOfWeekHeader: @Composable BoxScope.(List<DayOfWeek>) -> Unit = { DefaultDaysOfWeekHeader(it) },
monthContainer: @Composable (content: @Composable (PaddingValues) -> Unit) -> Unit = { content ->
Box { content(PaddingValues()) }
},
) {
val initialMonth = remember { calendarState.monthState.currentMonth }
val daysOfWeek = remember(firstDayOfWeek) {
DayOfWeek.values().rotateRight(DaysInAWeek - firstDayOfWeek.ordinal)
}
Column(
modifier = modifier,
) {
monthHeader(calendarState.monthState)
if (horizontalSwipeEnabled) {
MonthPager(
initialMonth = initialMonth,
showAdjacentMonths = showAdjacentMonths,
monthState = calendarState.monthState,
selectionState = calendarState.selectionState,
today = today,
weekDaysScrollEnabled = weekDaysScrollEnabled,
daysOfWeek = daysOfWeek,
dayContent = dayContent,
weekHeader = daysOfWeekHeader,
monthContainer = monthContainer,
)
} else {
MonthContent(
modifier = Modifier.fillMaxWidth(),
currentMonth = calendarState.monthState.currentMonth,
showAdjacentMonths = showAdjacentMonths,
selectionState = calendarState.selectionState,
today = today,
weekDaysScrollEnabled = weekDaysScrollEnabled,
daysOfWeek = daysOfWeek,
dayContent = dayContent,
weekHeader = daysOfWeekHeader,
monthContainer = monthContainer,
)
}
}
}
/**
* Helper function for providing a [CalendarState] implementation with selection mechanism.
*
* @param initialMonth initially rendered month
* @param initialSelection initial selection of the composable
* @param initialSelectionMode initial mode of the selection
* @param confirmSelectionChange callback for optional side-effects handling and vetoing the state change
* @param minMonth first month that can be shown
* @param maxMonth last month that can be shown
*/
@Composable
@SuppressLint("NewApi")
public fun rememberSelectableCalendarState(
initialMonth: YearMonth = YearMonth.now(),
minMonth: YearMonth = initialMonth.minusMonths(DefaultCalendarPagerRange),
maxMonth: YearMonth = initialMonth.plusMonths(DefaultCalendarPagerRange),
initialSelection: List<LocalDate> = emptyList(),
initialSelectionMode: SelectionMode = SelectionMode.Single,
confirmSelectionChange: (newValue: List<LocalDate>) -> Boolean = { true },
monthState: MonthState = rememberSaveable(saver = MonthState.Saver()) {
MonthState(
initialMonth = initialMonth,
minMonth = minMonth,
maxMonth = maxMonth
)
},
selectionState: DynamicSelectionState = rememberSaveable(
saver = DynamicSelectionState.Saver(confirmSelectionChange),
) {
DynamicSelectionState(confirmSelectionChange, initialSelection, initialSelectionMode)
},
): CalendarState<DynamicSelectionState> = remember { CalendarState(monthState, selectionState) }
/**
* Helper function for providing a [CalendarState] implementation without a selection mechanism.
*
* @param initialMonth initially rendered month
* @param minMonth first month that can be shown
* @param maxMonth last month that can be shown
*/
@Composable
@SuppressLint("NewApi")
public fun rememberCalendarState(
initialMonth: YearMonth = YearMonth.now(),
minMonth: YearMonth = initialMonth.minusMonths(DefaultCalendarPagerRange),
maxMonth: YearMonth = initialMonth.plusMonths(DefaultCalendarPagerRange),
monthState: MonthState = rememberSaveable(saver = MonthState.Saver()) {
MonthState(
initialMonth = initialMonth,
minMonth = minMonth,
maxMonth = maxMonth
)
},
): CalendarState<EmptySelectionState> = remember { CalendarState(monthState, EmptySelectionState) }
internal const val DefaultCalendarPagerRange = 10_000L
@@ -0,0 +1,264 @@
@file:Suppress("MatchingDeclarationName", "LongParameterList")
package io.github.boguszpawlowski.composecalendar
import android.annotation.SuppressLint
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import io.github.boguszpawlowski.composecalendar.day.DayState
import io.github.boguszpawlowski.composecalendar.day.DefaultDay
import io.github.boguszpawlowski.composecalendar.header.WeekState
import io.github.boguszpawlowski.composecalendar.selection.DynamicSelectionState
import io.github.boguszpawlowski.composecalendar.selection.EmptySelectionState
import io.github.boguszpawlowski.composecalendar.selection.SelectionMode
import io.github.boguszpawlowski.composecalendar.selection.SelectionState
import io.github.boguszpawlowski.composecalendar.week.DaysInAWeek
import io.github.boguszpawlowski.composecalendar.week.DefaultDaysOfWeekHeader
import io.github.boguszpawlowski.composecalendar.week.Week
import io.github.boguszpawlowski.composecalendar.week.WeekContent
import io.github.boguszpawlowski.composecalendar.week.WeekPager
import io.github.boguszpawlowski.composecalendar.week.getWeekDays
import io.github.boguszpawlowski.composecalendar.week.rotateRight
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.temporal.WeekFields
import java.util.Locale
import io.github.boguszpawlowski.composecalendar.header.DefaultWeekHeader as DefaultProperWeekHeader
/**
* State of the week calendar composable
*
* @property weekState currently showed week
* @property selectionState handler for the calendar's selection
*/
@Stable
public class WeekCalendarState<T : SelectionState>(
public val weekState: WeekState,
public val selectionState: T,
)
/**
* [WeekCalendar] implementation using a [DynamicSelectionState] as a selection handler.
*
* * Basic usage:
* ```
* @Composable
* fun MainScreen() {
* SelectableWeekCalendar()
* }
* ```
*
* @param modifier
* @param firstDayOfWeek first day of a week, defaults to current locale's
* @param today current day, defaults to [LocalDate.now]
* @param horizontalSwipeEnabled whenever user is able to change the week by horizontal swipe
* @param weekDaysScrollEnabled if the week days should be scrollable
* @param calendarState state of the composable
* @param dayContent composable rendering the current day
* @param weekHeader header for showing the current week and controls for changing it
* @param daysOfWeekHeader header for showing captions for each day of week
*/
@Composable
public fun SelectableWeekCalendar(
modifier: Modifier = Modifier,
firstDayOfWeek: DayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek,
today: LocalDate = LocalDate.now(),
horizontalSwipeEnabled: Boolean = true,
weekDaysScrollEnabled: Boolean = true,
calendarState: WeekCalendarState<DynamicSelectionState> = rememberSelectableWeekCalendarState(),
dayContent: @Composable BoxScope.(DayState<DynamicSelectionState>) -> Unit = { DefaultDay(it) },
weekHeader: @Composable ColumnScope.(WeekState) -> Unit = {
DefaultProperWeekHeader(it)
},
daysOfWeekHeader: @Composable BoxScope.(List<DayOfWeek>) -> Unit = { DefaultDaysOfWeekHeader(it) },
) {
WeekCalendar(
modifier = modifier,
calendarState = calendarState,
today = today,
horizontalSwipeEnabled = horizontalSwipeEnabled,
firstDayOfWeek = firstDayOfWeek,
weekDaysScrollEnabled = weekDaysScrollEnabled,
dayContent = dayContent,
weekHeader = weekHeader,
daysOfWeekHeader = daysOfWeekHeader,
)
}
/**
* [WeekCalendar] implementation without any mechanism for the selection.
*
* * Basic usage:
* ```
* @Composable
* fun MainScreen() {
* StaticWeekCalendar()
* }
* ```
*
* @param modifier
* @param firstDayOfWeek first day of a week, defaults to current locale's
* @param today current day, defaults to [LocalDate.now]
* @param horizontalSwipeEnabled whenever user is able to change the week by horizontal swipe
* @param calendarState state of the composable
* @param dayContent composable rendering the current day
* @param weekHeader header for showing the current week and controls for changing it
* @param daysOfWeekHeader header for showing captions for each day of week
*/
@Composable
@SuppressLint("NewApi")
public fun StaticWeekCalendar(
modifier: Modifier = Modifier,
firstDayOfWeek: DayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek,
today: LocalDate = LocalDate.now(),
horizontalSwipeEnabled: Boolean = true,
calendarState: WeekCalendarState<EmptySelectionState> = rememberWeekCalendarState(),
weekDaysScrollEnabled: Boolean = true,
dayContent: @Composable BoxScope.(DayState<EmptySelectionState>) -> Unit = { DefaultDay(it) },
weekHeader: @Composable ColumnScope.(WeekState) -> Unit = {
DefaultProperWeekHeader(it)
},
daysOfWeekHeader: @Composable BoxScope.(List<DayOfWeek>) -> Unit = { DefaultDaysOfWeekHeader(it) },
) {
WeekCalendar(
modifier = modifier,
calendarState = calendarState,
today = today,
horizontalSwipeEnabled = horizontalSwipeEnabled,
firstDayOfWeek = firstDayOfWeek,
weekDaysScrollEnabled = weekDaysScrollEnabled,
dayContent = dayContent,
weekHeader = weekHeader,
daysOfWeekHeader = daysOfWeekHeader,
)
}
/**
* Composable for showing a week calendar. The calendar state has to be provided by the call site. If you
* want to use built-in implementation, check out:
* [SelectableWeekCalendar] - calendar composable handling selection that can be changed at runtime
* [StaticWeekCalendar] - calendar without any mechanism for selection
*
* @param modifier
* @param firstDayOfWeek first day of a week, defaults to current locale's
* @param today current day, defaults to [LocalDate.now]
* @param horizontalSwipeEnabled whenever user is able to change the week by horizontal swipe
* @param calendarState state of the composable
* @param dayContent composable rendering the current day
* @param weekHeader header for showing the current week and controls for changing it
* @param daysOfWeekHeader header for showing captions for each day of week
*/
@Composable
@SuppressLint("NewApi")
public fun <T : SelectionState> WeekCalendar(
calendarState: WeekCalendarState<T>,
modifier: Modifier = Modifier,
today: LocalDate = LocalDate.now(),
firstDayOfWeek: DayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek,
horizontalSwipeEnabled: Boolean = true,
weekDaysScrollEnabled: Boolean = true,
dayContent: @Composable BoxScope.(DayState<T>) -> Unit = { DefaultDay(it) },
weekHeader: @Composable ColumnScope.(WeekState) -> Unit = {
DefaultProperWeekHeader(it)
},
daysOfWeekHeader: @Composable BoxScope.(List<DayOfWeek>) -> Unit = { DefaultDaysOfWeekHeader(it) },
) {
val initialWeek = remember { calendarState.weekState.currentWeek }
val daysOfWeek = remember(firstDayOfWeek) {
DayOfWeek.values().rotateRight(DaysInAWeek - firstDayOfWeek.ordinal)
}
Column(
modifier = modifier,
) {
weekHeader(calendarState.weekState)
if (horizontalSwipeEnabled) {
WeekPager(
initialWeek = initialWeek,
daysOfWeek = daysOfWeek,
weekState = calendarState.weekState,
selectionState = calendarState.selectionState,
today = today,
weekDaysScrollEnabled = weekDaysScrollEnabled,
dayContent = dayContent,
daysOfWeekHeader = daysOfWeekHeader,
)
} else {
WeekContent(
daysOfWeek = daysOfWeek,
weekDays = calendarState.weekState.currentWeek.getWeekDays(today),
selectionState = calendarState.selectionState,
weekDaysScrollEnabled = weekDaysScrollEnabled,
dayContent = dayContent,
daysOfWeekHeader = daysOfWeekHeader,
)
}
}
}
/**
* Helper function for providing a [WeekCalendarState] implementation with selection mechanism.
*
* @param firstDayOfWeek first day of a week, defaults to current locale's
* @param initialWeek initially rendered month
* @param initialSelection initial selection of the composable
* @param initialSelectionMode initial mode of the selection
* @param confirmSelectionChange callback for optional side-effects handling and vetoing the state change
* @param minWeek first week that can be shown
* @param maxWeek last week that can be shown
*/
@Composable
@SuppressLint("NewApi")
public fun rememberSelectableWeekCalendarState(
firstDayOfWeek: DayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek,
initialWeek: Week = Week.now(firstDayOfWeek),
minWeek: Week = initialWeek.minusWeeks(DefaultCalendarPagerRange),
maxWeek: Week = initialWeek.plusWeeks(DefaultCalendarPagerRange),
initialSelection: List<LocalDate> = emptyList(),
initialSelectionMode: SelectionMode = SelectionMode.Single,
confirmSelectionChange: (newValue: List<LocalDate>) -> Boolean = { true },
weekState: WeekState = rememberSaveable(saver = WeekState.Saver()) {
WeekState(
initialWeek = initialWeek,
minWeek = minWeek,
maxWeek = maxWeek,
)
},
selectionState: DynamicSelectionState = rememberSaveable(
saver = DynamicSelectionState.Saver(confirmSelectionChange),
) {
DynamicSelectionState(confirmSelectionChange, initialSelection, initialSelectionMode)
},
): WeekCalendarState<DynamicSelectionState> =
remember { WeekCalendarState(weekState, selectionState) }
/**
* Helper function for providing a [WeekCalendarState] implementation without a selection mechanism.
*
* @param firstDayOfWeek first day of a week, defaults to current locale's
* @param initialWeek initially rendered week
* @param minWeek first week that can be shown
* @param maxWeek last week that can be shown
*/
@Composable
@SuppressLint("NewApi")
public fun rememberWeekCalendarState(
firstDayOfWeek: DayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek,
initialWeek: Week = Week.now(firstDayOfWeek),
minWeek: Week = initialWeek.minusWeeks(DefaultCalendarPagerRange),
maxWeek: Week = initialWeek.plusWeeks(DefaultCalendarPagerRange),
weekState: WeekState = rememberSaveable(saver = WeekState.Saver()) {
WeekState(
initialWeek = initialWeek,
minWeek = minWeek,
maxWeek = maxWeek,
)
},
): WeekCalendarState<EmptySelectionState> =
remember { WeekCalendarState(weekState, EmptySelectionState) }
@@ -0,0 +1,16 @@
package io.github.boguszpawlowski.composecalendar.day
import java.time.LocalDate
/**
* Container for basic info about the displayed day
*
* @param date local date of the day
* @param isCurrentDay whenever the day is the today's date
* @param isFromCurrentMonth whenever the day is from currently rendered month
*/
public interface Day {
public val date: LocalDate
public val isCurrentDay: Boolean
public val isFromCurrentMonth: Boolean
}
@@ -0,0 +1,16 @@
package io.github.boguszpawlowski.composecalendar.day
import androidx.compose.runtime.Stable
import io.github.boguszpawlowski.composecalendar.selection.EmptySelectionState
import io.github.boguszpawlowski.composecalendar.selection.SelectionState
public typealias NonSelectableDayState = DayState<EmptySelectionState>
/**
* Contains information about current selection as well as date of rendered day
*/
@Stable
public data class DayState<T : SelectionState>(
private val day: Day,
val selectionState: T,
) : Day by day
@@ -0,0 +1,63 @@
package io.github.boguszpawlowski.composecalendar.day
import android.annotation.SuppressLint
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Card
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import io.github.boguszpawlowski.composecalendar.selection.SelectionState
import java.time.LocalDate
/**
* Default implementation for day content. It supports different appearance for days from
* current and adjacent month, as well as current day and selected day
*
* @param selectionColor color of the border, when day is selected
* @param currentDayColor color of content for the current date
* @param onClick callback for interacting with day clicks
*/
@Composable
@SuppressLint("NewApi")
public fun <T : SelectionState> DefaultDay(
state: DayState<T>,
modifier: Modifier = Modifier,
selectionColor: Color = MaterialTheme.colors.secondary,
currentDayColor: Color = MaterialTheme.colors.primary,
onClick: (LocalDate) -> Unit = {},
) {
val date = state.date
val selectionState = state.selectionState
val isSelected = selectionState.isDateSelected(date)
Card(
modifier = modifier
.aspectRatio(1f)
.padding(2.dp),
elevation = if (state.isFromCurrentMonth) 4.dp else 0.dp,
border = if (state.isCurrentDay) BorderStroke(1.dp, currentDayColor) else null,
contentColor = if (isSelected) selectionColor else contentColorFor(
backgroundColor = MaterialTheme.colors.surface
)
) {
Box(
modifier = Modifier.clickable {
onClick(date)
selectionState.onDateSelected(date)
},
contentAlignment = Alignment.Center,
) {
Text(text = date.dayOfMonth.toString())
}
}
}
@@ -0,0 +1,11 @@
package io.github.boguszpawlowski.composecalendar.day
import androidx.compose.runtime.Immutable
import java.time.LocalDate
@Immutable
internal class WeekDay(
override val date: LocalDate,
override val isCurrentDay: Boolean,
override val isFromCurrentMonth: Boolean
) : Day
@@ -0,0 +1,97 @@
package io.github.boguszpawlowski.composecalendar.debug
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
import kotlin.math.min
/**
* A [Modifier] that draws a border around elements that are recomposing. The border increases in
* size and interpolates from red to green as more recompositions occur before a timeout.
*/
@Stable
internal fun Modifier.recomposeHighlighter(): Modifier = this.then(recomposeModifier)
// Use a single instance + @Stable to ensure that recompositions can enable skipping optimizations
// Modifier.composed will still remember unique data per call site.
private val recomposeModifier =
Modifier.composed(inspectorInfo = debugInspectorInfo { name = "recomposeHighlighter" }) {
// The total number of compositions that have occurred. We're not using a State<> here be
// able to read/write the value without invalidating (which would cause infinite
// recomposition).
val totalCompositions = remember { arrayOf(0L) }
totalCompositions[0]++
// The value of totalCompositions at the last timeout.
val totalCompositionsAtLastTimeout = remember { mutableStateOf(0L) }
// Start the timeout, and reset everytime there's a recomposition. (Using totalCompositions
// as the key is really just to cause the timer to restart every composition).
LaunchedEffect(totalCompositions[0]) {
delay(3000)
totalCompositionsAtLastTimeout.value = totalCompositions[0]
}
Modifier.drawWithCache {
onDrawWithContent {
// Draw actual content.
drawContent()
// Below is to draw the highlight, if necessary. A lot of the logic is copied from
// Modifier.border
val numCompositionsSinceTimeout =
totalCompositions[0] - totalCompositionsAtLastTimeout.value
val hasValidBorderParams = size.minDimension > 0f
if (!hasValidBorderParams || numCompositionsSinceTimeout <= 0) {
return@onDrawWithContent
}
val (color, strokeWidthPx) =
when (numCompositionsSinceTimeout) {
// We need at least one composition to draw, so draw the smallest border
// color in blue.
1L -> Color.Blue to 1f
// 2 compositions is _probably_ okay.
2L -> Color.Green to 2.dp.toPx()
// 3 or more compositions before timeout may indicate an issue. lerp the
// color from yellow to red, and continually increase the border size.
else -> lerp(
Color.Yellow.copy(alpha = 0.8f),
Color.Red.copy(alpha = 0.5f),
min(1f, (numCompositionsSinceTimeout - 1).toFloat() / 100f)
) to numCompositionsSinceTimeout.toInt().dp.toPx()
}
val halfStroke = strokeWidthPx / 2
val topLeft = Offset(halfStroke, halfStroke)
val borderSize = Size(size.width - strokeWidthPx, size.height - strokeWidthPx)
val fillArea = strokeWidthPx * 2 > size.minDimension
val rectTopLeft = if (fillArea) Offset.Zero else topLeft
val size = if (fillArea) size else borderSize
val style = if (fillArea) Fill else Stroke(strokeWidthPx)
drawRect(
brush = SolidColor(color),
topLeft = rectTopLeft,
size = size,
style = style
)
}
}
}
@@ -0,0 +1,87 @@
package io.github.boguszpawlowski.composecalendar.header
import android.annotation.SuppressLint
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.width
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowLeft
import androidx.compose.material.icons.filled.KeyboardArrowRight
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp
import java.time.format.TextStyle.FULL
import java.util.Locale
/**
* Default implementation of month header, shows current month and year, as well as
* 2 arrows for changing currently showed month
*/
@Composable
@SuppressLint("NewApi")
public fun DefaultMonthHeader(
monthState: MonthState,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
DecrementButton(monthState = monthState)
Text(
modifier = Modifier.testTag("MonthLabel"),
text = monthState.currentMonth.month
.getDisplayName(FULL, Locale.getDefault())
.lowercase()
.replaceFirstChar { it.titlecase() },
style = MaterialTheme.typography.h4,
)
Spacer(modifier = Modifier.width(8.dp))
Text(text = monthState.currentMonth.year.toString(), style = MaterialTheme.typography.h4)
IncrementButton(monthState = monthState)
}
}
@Composable
private fun DecrementButton(
monthState: MonthState,
) {
IconButton(
modifier = Modifier.testTag("Decrement"),
enabled = monthState.currentMonth > monthState.minMonth,
onClick = { monthState.currentMonth = monthState.currentMonth.minusMonths(1) }
) {
Image(
imageVector = Icons.Default.KeyboardArrowLeft,
colorFilter = ColorFilter.tint(MaterialTheme.colors.onSurface),
contentDescription = "Previous",
)
}
}
@Composable
private fun IncrementButton(
monthState: MonthState,
) {
IconButton(
modifier = Modifier.testTag("Increment"),
enabled = monthState.currentMonth < monthState.maxMonth,
onClick = { monthState.currentMonth = monthState.currentMonth.plusMonths(1) }
) {
Image(
imageVector = Icons.Default.KeyboardArrowRight,
colorFilter = ColorFilter.tint(MaterialTheme.colors.onSurface),
contentDescription = "Next",
)
}
}
@@ -0,0 +1,91 @@
package io.github.boguszpawlowski.composecalendar.header
import android.annotation.SuppressLint
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.width
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowLeft
import androidx.compose.material.icons.filled.KeyboardArrowRight
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp
import java.time.format.TextStyle.FULL
import java.util.Locale
/**
* Default implementation of week header, shows current month and year, as well as
* 2 arrows for changing currently showed month
*/
@Composable
@Suppress("LongMethod")
@SuppressLint("NewApi")
public fun DefaultWeekHeader(
weekState: WeekState,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
DecrementButton(weekState = weekState)
Text(
modifier = Modifier.testTag("WeekLabel"),
text = weekState.currentWeek.yearMonth.month
.getDisplayName(FULL, Locale.getDefault())
.lowercase()
.replaceFirstChar { it.titlecase() },
style = MaterialTheme.typography.h4,
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = weekState.currentWeek.yearMonth.year.toString(),
style = MaterialTheme.typography.h4
)
IncrementButton(monthState = weekState)
}
}
@Composable
private fun DecrementButton(
weekState: WeekState,
) {
IconButton(
modifier = Modifier.testTag("Decrement"),
enabled = weekState.currentWeek > weekState.minWeek,
onClick = { weekState.currentWeek = weekState.currentWeek.dec() }
) {
Image(
imageVector = Icons.Default.KeyboardArrowLeft,
colorFilter = ColorFilter.tint(MaterialTheme.colors.onSurface),
contentDescription = "Previous",
)
}
}
@Composable
private fun IncrementButton(
monthState: WeekState,
) {
IconButton(
modifier = Modifier.testTag("Increment"),
enabled = monthState.currentWeek < monthState.maxWeek,
onClick = { monthState.currentWeek = monthState.currentWeek.inc() }
) {
Image(
imageVector = Icons.Default.KeyboardArrowRight,
colorFilter = ColorFilter.tint(MaterialTheme.colors.onSurface),
contentDescription = "Next",
)
}
}
@@ -0,0 +1,76 @@
package io.github.boguszpawlowski.composecalendar.header
import android.annotation.SuppressLint
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.mapSaver
import androidx.compose.runtime.setValue
import java.time.YearMonth
@Suppress("FunctionName") // Factory function
public fun MonthState(
initialMonth: YearMonth,
minMonth: YearMonth,
maxMonth: YearMonth,
): MonthState = MonthStateImpl(initialMonth, minMonth, maxMonth)
@Stable
public interface MonthState {
public var currentMonth: YearMonth
public var minMonth: YearMonth
public var maxMonth: YearMonth
public companion object {
public fun Saver(): Saver<MonthState, Any> = mapSaver(
save = { monthState ->
mapOf(
"currentMonth" to monthState.currentMonth.toString(),
"minMonth" to monthState.minMonth.toString(),
"maxMonth" to monthState.maxMonth.toString(),
)
},
restore = { restoreMap ->
MonthState(
YearMonth.parse(restoreMap["currentMonth"] as String),
YearMonth.parse(restoreMap["minMonth"] as String),
YearMonth.parse(restoreMap["maxMonth"] as String),
)
}
)
}
}
@Stable
@SuppressLint("NewApi")
private class MonthStateImpl(
initialMonth: YearMonth,
minMonth: YearMonth,
maxMonth: YearMonth,
) : MonthState {
private var _currentMonth by mutableStateOf(initialMonth)
private var _minMonth by mutableStateOf(minMonth)
private var _maxMonth by mutableStateOf(maxMonth)
override var currentMonth: YearMonth
get() = _currentMonth
set(value) {
_currentMonth = value
}
override var minMonth: YearMonth
get() = _minMonth
set(value) {
if (value > _maxMonth) return
_minMonth = value
}
override var maxMonth: YearMonth
get() = _maxMonth
set(value) {
if (value < _minMonth) return
_maxMonth = value
}
}
@@ -0,0 +1,86 @@
package io.github.boguszpawlowski.composecalendar.header
import android.annotation.SuppressLint
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.mapSaver
import androidx.compose.runtime.setValue
import io.github.boguszpawlowski.composecalendar.week.Week
import java.time.LocalDate
@Suppress("FunctionName") // Factory function
public fun WeekState(
initialWeek: Week,
minWeek: Week,
maxWeek: Week,
): WeekState = WeekStateImpl(
initialWeek = initialWeek,
minWeek = minWeek,
maxWeek = maxWeek,
)
@Stable
@SuppressLint("NewApi")
public interface WeekState {
public var currentWeek: Week
public var minWeek: Week
public var maxWeek: Week
public companion object {
@Suppress("FunctionName") // Factory function
public fun Saver(): Saver<WeekState, Any> = mapSaver(
save = { weekState ->
mapOf(
CurrentWeekKey to weekState.currentWeek.days[0].toString(),
MinWeekKey to weekState.minWeek.days[0].toString(),
MaxWeekKey to weekState.maxWeek.days[0].toString(),
)
},
restore = { restoreMap ->
WeekState(
initialWeek = Week(firstDay = LocalDate.parse(restoreMap[CurrentWeekKey] as String)),
minWeek = Week(firstDay = LocalDate.parse(restoreMap[MinWeekKey] as String)),
maxWeek = Week(firstDay = LocalDate.parse(restoreMap[MaxWeekKey] as String)),
)
}
)
private const val CurrentWeekKey = "CurrentWeek"
private const val MinWeekKey = "MinWeek"
private const val MaxWeekKey = "MaxWeek"
}
}
@Stable
private class WeekStateImpl(
initialWeek: Week,
minWeek: Week,
maxWeek: Week,
) : WeekState {
private var _currentWeek by mutableStateOf(initialWeek)
private var _minWeek by mutableStateOf(minWeek)
private var _maxWeek by mutableStateOf(maxWeek)
override var currentWeek: Week
get() = _currentWeek
set(value) {
_currentWeek = value
}
override var minWeek: Week
get() = _minWeek
set(value) {
if (value > _maxWeek) return
_minWeek = value
}
override var maxWeek: Week
get() = _maxWeek
set(value) {
if (value < _minWeek) return
_maxWeek = value
}
}
@@ -0,0 +1,162 @@
package io.github.boguszpawlowski.composecalendar.month
import androidx.compose.animation.rememberSplineBasedDecay
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import dev.chrisbanes.snapper.ExperimentalSnapperApi
import dev.chrisbanes.snapper.SnapOffsets
import dev.chrisbanes.snapper.SnapperFlingBehaviorDefaults
import dev.chrisbanes.snapper.SnapperLayoutInfo
import dev.chrisbanes.snapper.rememberSnapperFlingBehavior
import io.github.boguszpawlowski.composecalendar.day.DayState
import io.github.boguszpawlowski.composecalendar.header.MonthState
import io.github.boguszpawlowski.composecalendar.selection.SelectionState
import io.github.boguszpawlowski.composecalendar.week.WeekRow
import io.github.boguszpawlowski.composecalendar.week.getWeeks
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.YearMonth
import java.time.temporal.ChronoUnit
@OptIn(ExperimentalSnapperApi::class)
@Composable
@Suppress("LongMethod", "NewApi")
internal fun <T : SelectionState> MonthPager(
initialMonth: YearMonth,
showAdjacentMonths: Boolean,
selectionState: T,
monthState: MonthState,
daysOfWeek: List<DayOfWeek>,
today: LocalDate,
modifier: Modifier = Modifier,
weekDaysScrollEnabled: Boolean = true,
dayContent: @Composable BoxScope.(DayState<T>) -> Unit,
weekHeader: @Composable BoxScope.(List<DayOfWeek>) -> Unit,
monthContainer: @Composable (content: @Composable (PaddingValues) -> Unit) -> Unit,
) {
val coroutineScope = rememberCoroutineScope()
val initialFirstVisibleItemIndex = remember(initialMonth, monthState.minMonth) {
ChronoUnit.MONTHS.between(monthState.minMonth, initialMonth).toInt()
}
val listState = rememberLazyListState(
initialFirstVisibleItemIndex = initialFirstVisibleItemIndex,
)
val flingBehavior = rememberSnapperFlingBehavior(
lazyListState = listState,
snapOffsetForItem = SnapOffsets.Start,
springAnimationSpec = SnapperFlingBehaviorDefaults.SpringAnimationSpec,
decayAnimationSpec = rememberSplineBasedDecay(),
snapIndex = coerceSnapIndex,
)
val monthListState = remember {
MonthListState(
coroutineScope = coroutineScope,
monthState = monthState,
listState = listState,
)
}
Column(modifier = Modifier.fillMaxWidth()) {
if (weekDaysScrollEnabled.not()) {
Box(
modifier = Modifier
.wrapContentHeight()
.fillMaxWidth(),
content = { weekHeader(daysOfWeek) },
)
}
val pagerCount = remember(monthState.minMonth, monthState.maxMonth) {
ChronoUnit.MONTHS.between(monthState.minMonth, monthState.maxMonth).toInt() + 1
}
LazyRow(
modifier = modifier.testTag("MonthPager"),
state = listState,
flingBehavior = flingBehavior,
verticalAlignment = Alignment.Top,
) {
items(
count = pagerCount,
key = { index ->
monthListState.getMonthForPage(index).let { "${it.year}-${it.monthValue}" }
}
) { index ->
MonthContent(
modifier = Modifier.fillParentMaxWidth(),
showAdjacentMonths = showAdjacentMonths,
selectionState = selectionState,
currentMonth = monthListState.getMonthForPage(index),
today = today,
weekDaysScrollEnabled = weekDaysScrollEnabled,
daysOfWeek = daysOfWeek,
dayContent = dayContent,
weekHeader = weekHeader,
monthContainer = monthContainer
)
}
}
}
}
@Composable
internal fun <T : SelectionState> MonthContent(
showAdjacentMonths: Boolean,
selectionState: T,
currentMonth: YearMonth,
daysOfWeek: List<DayOfWeek>,
today: LocalDate,
modifier: Modifier = Modifier,
weekDaysScrollEnabled: Boolean = true,
dayContent: @Composable BoxScope.(DayState<T>) -> Unit,
weekHeader: @Composable BoxScope.(List<DayOfWeek>) -> Unit,
monthContainer: @Composable (content: @Composable (PaddingValues) -> Unit) -> Unit,
) {
Column {
if (weekDaysScrollEnabled) {
Box(
modifier = modifier
.wrapContentHeight(),
content = { weekHeader(daysOfWeek) },
)
}
monthContainer { paddingValues ->
Column(
modifier = modifier
.padding(paddingValues)
) {
currentMonth.getWeeks(
includeAdjacentMonths = showAdjacentMonths,
firstDayOfTheWeek = daysOfWeek.first(),
today = today,
).forEach { week ->
WeekRow(
weekDays = week,
selectionState = selectionState,
dayContent = dayContent,
)
}
}
}
}
}
@OptIn(ExperimentalSnapperApi::class)
internal val coerceSnapIndex: (SnapperLayoutInfo, startIndex: Int, targetIndex: Int) -> Int =
{ _, startIndex, targetIndex ->
targetIndex
.coerceIn(startIndex - 1, startIndex + 1)
}
@@ -0,0 +1,74 @@
package io.github.boguszpawlowski.composecalendar.month
import android.annotation.SuppressLint
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.snapshotFlow
import io.github.boguszpawlowski.composecalendar.header.MonthState
import io.github.boguszpawlowski.composecalendar.util.throttleOnOffset
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import java.time.YearMonth
import java.time.temporal.ChronoUnit
@Stable
@SuppressLint("NewApi")
internal class MonthListState(
private val coroutineScope: CoroutineScope,
private val monthState: MonthState,
private val listState: LazyListState,
) {
private val currentFirstVisibleMonth by derivedStateOf {
getMonthForPage(listState.firstVisibleItemIndex)
}
init {
snapshotFlow { monthState.currentMonth }.onEach { month ->
moveToMonth(month)
}.launchIn(coroutineScope)
with(listState) {
snapshotFlow { currentFirstVisibleMonth }
.throttleOnOffset()
.onEach { newMonth ->
monthState.currentMonth = newMonth
}.launchIn(coroutineScope)
}
}
fun getMonthForPage(index: Int): YearMonth =
monthState.minMonth.plusMonths(index.toLong())
private fun moveToMonth(month: YearMonth) {
if (month == currentFirstVisibleMonth) return
coroutineScope.launch {
listState.animateScrollToItem(ChronoUnit.MONTHS.between(monthState.minMonth, month).toInt())
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as MonthListState
if (monthState != other.monthState) return false
if (listState != other.listState) return false
return true
}
override fun hashCode(): Int {
var result = monthState.hashCode()
result = 31 * result + listState.hashCode()
return result
}
}
private operator fun YearMonth.minus(other: YearMonth) =
ChronoUnit.MONTHS.between(other, this)
@@ -0,0 +1,35 @@
package io.github.boguszpawlowski.composecalendar.selection
import android.annotation.SuppressLint
import io.github.boguszpawlowski.composecalendar.util.addOrRemoveIfExists
import java.time.LocalDate
/**
* Helper class for calculating new selection, when using a [DynamicSelectionState] approach.
* @param date clicked on date
* @param selection current selection
* @param selectionMode current selection mode
* @returns new selection in a form of a list of local dates.
*/
@SuppressLint("NewApi")
public object DynamicSelectionHandler {
public fun calculateNewSelection(
date: LocalDate,
selection: List<LocalDate>,
selectionMode: SelectionMode,
): List<LocalDate> = when (selectionMode) {
SelectionMode.None -> emptyList()
SelectionMode.Single -> if (date == selection.firstOrNull()) {
emptyList()
} else {
listOf(date)
}
SelectionMode.Multiple -> selection.addOrRemoveIfExists(date)
SelectionMode.Period -> when {
date.isBefore(selection.startOrMax()) -> listOf(date)
date.isAfter(selection.startOrMax()) -> selection.fillUpTo(date)
date == selection.startOrMax() -> emptyList()
else -> selection
}
}
}
@@ -0,0 +1,15 @@
package io.github.boguszpawlowski.composecalendar.selection
import android.annotation.SuppressLint
import java.time.LocalDate
@SuppressLint("NewApi")
internal fun Collection<LocalDate>.startOrMax() = firstOrNull() ?: LocalDate.MAX
internal fun Collection<LocalDate>.endOrNull() = drop(1).lastOrNull()
@SuppressLint("NewApi")
internal fun Collection<LocalDate>.fillUpTo(date: LocalDate) =
(0..date.toEpochDay() - first().toEpochDay()).map {
first().plusDays(it)
}
@@ -0,0 +1,15 @@
package io.github.boguszpawlowski.composecalendar.selection
/**
* Selection modes for the [DynamicSelectionState]
* None - no selection allowed
* Single - only one date selected
* Multiple - multiple dates can be selected
* Period - period can be selected
*/
public enum class SelectionMode {
None,
Single,
Multiple,
Period,
}
@@ -0,0 +1,83 @@
package io.github.boguszpawlowski.composecalendar.selection
import android.annotation.SuppressLint
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.listSaver
import androidx.compose.runtime.setValue
import java.time.LocalDate
@Stable
public interface SelectionState {
public fun isDateSelected(date: LocalDate): Boolean = false
public fun onDateSelected(date: LocalDate) { }
}
/**
* Class that enables for dynamically changing selection modes in the runtime. Depending on the mode, selection changes differently.
* Mode can be varied by setting desired [SelectionMode] in the [selectionMode] mutable property.
* @param confirmSelectionChange return false from this callback to veto the selection change
*/
@Stable
@SuppressLint("NewApi")
public class DynamicSelectionState(
private val confirmSelectionChange: (newValue: List<LocalDate>) -> Boolean = { true },
selection: List<LocalDate>,
selectionMode: SelectionMode,
) : SelectionState {
private var _selection by mutableStateOf(selection)
private var _selectionMode by mutableStateOf(selectionMode)
public var selection: List<LocalDate>
get() = _selection
set(value) {
if (value != selection && confirmSelectionChange(value)) {
_selection = value
}
}
public var selectionMode: SelectionMode
get() = _selectionMode
set(value) {
if (value != selectionMode) {
_selection = emptyList()
_selectionMode = value
}
}
override fun isDateSelected(date: LocalDate): Boolean = selection.contains(date)
override fun onDateSelected(date: LocalDate) {
selection = DynamicSelectionHandler.calculateNewSelection(date, selection, selectionMode)
}
internal companion object {
@Suppress("FunctionName", "UNCHECKED_CAST") // Factory function
fun Saver(
confirmSelectionChange: (newValue: List<LocalDate>) -> Boolean,
): Saver<DynamicSelectionState, Any> =
listSaver(
save = { raw ->
listOf(raw.selectionMode, raw.selection.map { it.toString() })
},
restore = { restored ->
DynamicSelectionState(
confirmSelectionChange = confirmSelectionChange,
selectionMode = restored[0] as SelectionMode,
selection = (restored[1] as? List<String>)?.map { LocalDate.parse(it) }.orEmpty(),
)
}
)
}
}
@Immutable
public object EmptySelectionState : SelectionState {
override fun isDateSelected(date: LocalDate): Boolean = false
override fun onDateSelected(date: LocalDate): Unit = Unit
}
@@ -0,0 +1,13 @@
package io.github.boguszpawlowski.composecalendar.util
import java.time.DayOfWeek
import java.time.LocalDate
internal fun Collection<LocalDate>.addOrRemoveIfExists(date: LocalDate) =
if (contains(date)) {
this - date
} else {
this + date
}
internal infix fun DayOfWeek.daysUntil(other: DayOfWeek) = (7 + (value - other.value)) % 7
@@ -0,0 +1,19 @@
package io.github.boguszpawlowski.composecalendar.util
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.snapshotFlow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
context(LazyListState) internal fun <T> Flow<T>.throttleOnOffset() =
combine(
snapshotFlow { firstVisibleItemScrollOffset }
) { newMonth, offset ->
newMonth to (offset <= MinimalOffsetForEmit)
}.filter { (_, shouldUpdate) ->
shouldUpdate
}.map { (newValue, _) -> newValue }
private const val MinimalOffsetForEmit = 10
@@ -0,0 +1,7 @@
package io.github.boguszpawlowski.composecalendar.util
import java.time.YearMonth
internal operator fun YearMonth.dec() = this.minusMonths(1)
internal operator fun YearMonth.inc() = this.plusMonths(1)
@@ -0,0 +1,61 @@
package io.github.boguszpawlowski.composecalendar.week
import android.annotation.SuppressLint
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import java.time.DayOfWeek
import java.time.format.TextStyle.SHORT
import java.util.Locale
import kotlin.DeprecationLevel.WARNING
@Composable
@Deprecated(
level = WARNING,
replaceWith = ReplaceWith(
"DefaultDaysOfWeekHeader(daysOfWeek, modifier)",
"io.github.boguszpawlowski.composecalendar.week.DefaultDaysOfWeekHeader",
),
message = "Replace with DefaultDaysOfWeekHeader, DefaultWeekHeader will be removed in future versions"
)
@SuppressLint("NewApi")
public fun DefaultWeekHeader(
daysOfWeek: List<DayOfWeek>,
modifier: Modifier = Modifier,
) {
Row(modifier = modifier) {
daysOfWeek.forEach { dayOfWeek ->
Text(
textAlign = TextAlign.Center,
text = dayOfWeek.getDisplayName(SHORT, Locale.getDefault()),
modifier = modifier
.weight(1f)
.wrapContentHeight()
)
}
}
}
@Composable
@SuppressLint("NewApi")
public fun DefaultDaysOfWeekHeader(
daysOfWeek: List<DayOfWeek>,
modifier: Modifier = Modifier,
) {
Row(modifier = modifier) {
daysOfWeek.forEach { dayOfWeek ->
Text(
textAlign = TextAlign.Center,
text = dayOfWeek.getDisplayName(SHORT, Locale.getDefault()),
modifier = modifier
.weight(1f)
.wrapContentHeight()
)
}
}
}
internal fun <T> Array<T>.rotateRight(n: Int): List<T> = takeLast(n) + dropLast(n)
@@ -0,0 +1,55 @@
package io.github.boguszpawlowski.composecalendar.week
import android.annotation.SuppressLint
import io.github.boguszpawlowski.composecalendar.selection.fillUpTo
import io.github.boguszpawlowski.composecalendar.util.daysUntil
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.YearMonth
import java.time.temporal.ChronoUnit
import java.time.temporal.WeekFields
import java.util.Locale
@SuppressLint("NewApi")
public data class Week(
val days: List<LocalDate>,
) {
init {
require(days.size == DaysInAWeek)
}
internal constructor(firstDay: LocalDate) : this(
listOf(firstDay).fillUpTo(firstDay.plusDays((DaysInAWeek - 1).toLong()))
)
public val start: LocalDate get() = days.first()
public val end: LocalDate get() = days.last()
public val yearMonth: YearMonth = YearMonth.of(start.year, start.month)
public operator fun inc(): Week = plusWeeks(1)
public operator fun dec(): Week = plusWeeks(-1)
public operator fun compareTo(other: Week): Int = start.compareTo(other.start)
public fun minusWeeks(value: Long): Week = plusWeeks(-value)
public fun plusWeeks(value: Long): Week = copy(days = days.map { it.plusWeeks(value) })
public companion object {
public fun now(firstDayOfWeek: DayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek): Week {
val today = LocalDate.now()
val offset = today.dayOfWeek.daysUntil(firstDayOfWeek)
val firstDay = today.minusDays(offset.toLong())
return Week(firstDay)
}
}
}
@SuppressLint("NewApi")
public fun ChronoUnit.between(first: Week, other: Week): Int =
between(first.start, other.start).toInt()
@@ -0,0 +1,10 @@
package io.github.boguszpawlowski.composecalendar.week
import androidx.compose.runtime.Immutable
import io.github.boguszpawlowski.composecalendar.day.Day
@Immutable
internal data class WeekDays(
val isFirstWeekOfTheMonth: Boolean = false,
val days: List<Day>,
)
@@ -0,0 +1,70 @@
package io.github.boguszpawlowski.composecalendar.week
import android.annotation.SuppressLint
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.snapshotFlow
import io.github.boguszpawlowski.composecalendar.header.WeekState
import io.github.boguszpawlowski.composecalendar.util.throttleOnOffset
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import java.time.temporal.ChronoUnit
@Stable
@SuppressLint("NewApi")
internal class WeekListState(
private val coroutineScope: CoroutineScope,
private val weekState: WeekState,
private val listState: LazyListState,
) {
private val currentlyFirstVisibleMonth by derivedStateOf {
getWeekForPage(listState.firstVisibleItemIndex)
}
init {
snapshotFlow { weekState.currentWeek }.onEach { week ->
moveToWeek(week)
}.launchIn(coroutineScope)
with(listState) {
snapshotFlow { currentlyFirstVisibleMonth }
.throttleOnOffset()
.onEach { newMonth ->
weekState.currentWeek = newMonth
}.launchIn(coroutineScope)
}
}
fun getWeekForPage(index: Int): Week =
weekState.minWeek.plusWeeks(index.toLong())
private fun moveToWeek(week: Week) {
if (week == currentlyFirstVisibleMonth) return
coroutineScope.launch {
listState.animateScrollToItem(ChronoUnit.WEEKS.between(weekState.minWeek, week))
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as WeekListState
if (weekState != other.weekState) return false
if (listState != other.listState) return false
return true
}
override fun hashCode(): Int {
var result = weekState.hashCode()
result = 31 * result + listState.hashCode()
return result
}
}
@@ -0,0 +1,150 @@
package io.github.boguszpawlowski.composecalendar.week
import android.annotation.SuppressLint
import androidx.compose.animation.rememberSplineBasedDecay
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import dev.chrisbanes.snapper.ExperimentalSnapperApi
import dev.chrisbanes.snapper.SnapOffsets
import dev.chrisbanes.snapper.SnapperFlingBehaviorDefaults
import dev.chrisbanes.snapper.rememberSnapperFlingBehavior
import io.github.boguszpawlowski.composecalendar.day.DayState
import io.github.boguszpawlowski.composecalendar.day.WeekDay
import io.github.boguszpawlowski.composecalendar.header.WeekState
import io.github.boguszpawlowski.composecalendar.month.coerceSnapIndex
import io.github.boguszpawlowski.composecalendar.selection.SelectionState
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.temporal.ChronoUnit
@OptIn(ExperimentalSnapperApi::class)
@Composable
@Suppress("LongMethod")
@SuppressLint("NewApi")
internal fun <T : SelectionState> WeekPager(
initialWeek: Week,
selectionState: T,
weekState: WeekState,
daysOfWeek: List<DayOfWeek>,
today: LocalDate,
modifier: Modifier = Modifier,
weekDaysScrollEnabled: Boolean = true,
dayContent: @Composable BoxScope.(DayState<T>) -> Unit,
daysOfWeekHeader: @Composable BoxScope.(List<DayOfWeek>) -> Unit,
) {
val coroutineScope = rememberCoroutineScope()
val initialFirstVisibleItemIndex = remember(initialWeek, weekState.minWeek) {
ChronoUnit.WEEKS.between(weekState.minWeek, initialWeek)
}
val listState = rememberLazyListState(
initialFirstVisibleItemIndex = initialFirstVisibleItemIndex,
)
val flingBehavior = rememberSnapperFlingBehavior(
lazyListState = listState,
snapOffsetForItem = SnapOffsets.Start,
springAnimationSpec = SnapperFlingBehaviorDefaults.SpringAnimationSpec,
decayAnimationSpec = rememberSplineBasedDecay(),
snapIndex = coerceSnapIndex,
)
val weekListState = remember {
WeekListState(
coroutineScope = coroutineScope,
weekState = weekState,
listState = listState,
)
}
Column(
modifier = modifier,
) {
if (weekDaysScrollEnabled.not()) {
Box(
modifier = Modifier
.wrapContentHeight()
.fillMaxWidth(),
content = { daysOfWeekHeader(daysOfWeek) },
)
}
val pagerCount = remember(weekState.minWeek, weekState.maxWeek) {
ChronoUnit.WEEKS.between(weekState.minWeek, weekState.maxWeek) + 1
}
LazyRow(
modifier = modifier.testTag("WeekPager"),
state = listState,
flingBehavior = flingBehavior,
verticalAlignment = Alignment.Top,
) {
items(
count = pagerCount,
key = { index -> weekListState.getWeekForPage(index).start.let { "${it.month}-${it.dayOfMonth}" } },
) { index ->
WeekContent(
modifier = Modifier.fillParentMaxWidth(),
daysOfWeek = daysOfWeek,
weekDays = weekListState.getWeekForPage(index).getWeekDays(today = today),
selectionState = selectionState,
weekDaysScrollEnabled = weekDaysScrollEnabled,
dayContent = dayContent,
daysOfWeekHeader = daysOfWeekHeader,
)
}
}
}
}
@Composable
internal fun <T : SelectionState> WeekContent(
selectionState: T,
weekDays: WeekDays,
daysOfWeek: List<DayOfWeek>,
modifier: Modifier = Modifier,
weekDaysScrollEnabled: Boolean = true,
dayContent: @Composable BoxScope.(DayState<T>) -> Unit,
daysOfWeekHeader: @Composable BoxScope.(List<DayOfWeek>) -> Unit,
) {
Column(
modifier = modifier,
) {
if (weekDaysScrollEnabled) {
Box(
modifier = Modifier
.wrapContentHeight()
.fillMaxWidth(),
content = { daysOfWeekHeader(daysOfWeek) },
)
}
WeekRow(
weekDays = weekDays,
modifier = Modifier.fillMaxWidth(),
selectionState = selectionState,
dayContent = dayContent,
)
}
}
internal fun Week.getWeekDays(today: LocalDate): WeekDays {
val weekDays = days.map {
WeekDay(
date = it,
isCurrentDay = it == today,
isFromCurrentMonth = true,
)
}
return WeekDays(
isFirstWeekOfTheMonth = false,
days = weekDays,
)
}
@@ -0,0 +1,35 @@
package io.github.boguszpawlowski.composecalendar.week
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import io.github.boguszpawlowski.composecalendar.day.DayState
import io.github.boguszpawlowski.composecalendar.selection.SelectionState
@Composable
internal fun <T : SelectionState> WeekRow(
weekDays: WeekDays,
selectionState: T,
modifier: Modifier = Modifier,
dayContent: @Composable BoxScope.(DayState<T>) -> Unit
) {
Row(
modifier = modifier
.fillMaxWidth()
.wrapContentHeight(),
horizontalArrangement = if (weekDays.isFirstWeekOfTheMonth) Arrangement.End else Arrangement.Start
) {
weekDays.days.forEachIndexed { index, day ->
Box(
modifier = Modifier.fillMaxWidth(1f / (7 - index))
) {
dayContent(DayState(day, selectionState))
}
}
}
}
@@ -0,0 +1,52 @@
package io.github.boguszpawlowski.composecalendar.week
import android.annotation.SuppressLint
import io.github.boguszpawlowski.composecalendar.day.WeekDay
import io.github.boguszpawlowski.composecalendar.util.daysUntil
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.YearMonth
internal const val DaysInAWeek = 7
@SuppressLint("NewApi")
internal fun YearMonth.getWeeks(
includeAdjacentMonths: Boolean,
firstDayOfTheWeek: DayOfWeek,
today: LocalDate,
): List<WeekDays> {
val daysLength = lengthOfMonth()
val starOffset = atDay(1).dayOfWeek daysUntil firstDayOfTheWeek
val endOffset =
DaysInAWeek - (atDay(daysLength).dayOfWeek daysUntil firstDayOfTheWeek) - 1
return (1 - starOffset..daysLength + endOffset).chunked(DaysInAWeek).mapIndexed { index, days ->
WeekDays(
isFirstWeekOfTheMonth = index == 0,
days = days.mapNotNull { dayOfMonth ->
val (date, isFromCurrentMonth) = when (dayOfMonth) {
in Int.MIN_VALUE..0 -> if (includeAdjacentMonths) {
val previousMonth = this.minusMonths(1)
previousMonth.atDay(previousMonth.lengthOfMonth() + dayOfMonth) to false
} else {
return@mapNotNull null
}
in 1..daysLength -> atDay(dayOfMonth) to true
else -> if (includeAdjacentMonths) {
val previousMonth = this.plusMonths(1)
previousMonth.atDay(dayOfMonth - daysLength) to false
} else {
return@mapNotNull null
}
}
WeekDay(
date = date,
isFromCurrentMonth = isFromCurrentMonth,
isCurrentDay = date.equals(today),
)
}
)
}
}
@@ -0,0 +1,177 @@
@file:Suppress("UnderscoresInNumericLiterals")
package io.github.boguszpawlowski.composecalendar.selection
import io.github.boguszpawlowski.composecalendar.selection.SelectionMode.Multiple
import io.kotest.assertions.throwables.shouldNotThrowAny
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.collections.shouldContainExactly
import io.kotest.matchers.shouldBe
import java.time.LocalDate
import java.time.Month.APRIL
internal class SelectionStateTest : ShouldSpec({
val yesterday = LocalDate.of(2020, APRIL, 9)
val today = LocalDate.of(2020, APRIL, 10)
val tomorrow = LocalDate.of(2020, APRIL, 11)
context("Selection state with SelectionMode.None") {
should("not change selection after new value arrives") {
val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.None)
state.onDateSelected(LocalDate.now())
state.selection shouldBe emptyList()
}
should("be able to change if mode has been changed") {
val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.None)
state.selectionMode = SelectionMode.Single
state.onDateSelected(today)
state.selection shouldBe listOf(today)
}
}
context("Selection state with SelectionMode.Single") {
should("change state to single after day is selected") {
val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.Single)
state.onDateSelected(today)
state.selection shouldBe listOf(today)
}
should("change state to none when same day is selected") {
val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.Single)
state.onDateSelected(today)
state.onDateSelected(today)
state.selection shouldBe emptyList()
}
should("change to other day when selected") {
val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.Single)
state.onDateSelected(today)
state.onDateSelected(tomorrow)
state.selection shouldBe listOf(tomorrow)
}
should("not be mutable after selection mode is changed to None") {
val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.Single)
state.selectionMode = SelectionMode.None
state.onDateSelected(today)
state.selection shouldBe emptyList()
}
}
context("Selection state with SelectionMode.Multiple") {
should("allow for multiple days selected") {
val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.Multiple)
state.onDateSelected(today)
state.onDateSelected(tomorrow)
state.selection shouldContainExactly listOf(
today,
tomorrow
)
}
should("switch selection off once day is selected second time") {
val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.Multiple)
state.onDateSelected(today)
state.onDateSelected(tomorrow)
state.onDateSelected(today)
state.selection shouldContainExactly listOf(tomorrow)
}
}
context("Selection state with SelectionMode.Period") {
should("allow for period of days selected") {
val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.Period)
state.onDateSelected(today)
state.onDateSelected(tomorrow)
state.selection.first() shouldBe today
state.selection.last() shouldBe tomorrow
}
should("switch selection off once start day is selected") {
val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.Period)
state.onDateSelected(today)
state.onDateSelected(tomorrow)
state.onDateSelected(today)
state.selection shouldBe emptyList()
}
should("change end date once the date selected is between start and the end") {
val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.Period)
state.onDateSelected(yesterday)
state.onDateSelected(tomorrow)
state.onDateSelected(today)
state.selection.first() shouldBe yesterday
state.selection.last() shouldBe today
}
should("change start day once day before start is selected") {
val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.Period)
state.onDateSelected(today)
state.onDateSelected(tomorrow)
state.onDateSelected(yesterday)
state.selection.first() shouldBe yesterday
state.selection.endOrNull() shouldBe null
}
}
context("Selection State interface default values") {
val myInterfaceWithNoMethods = object : SelectionState {}
should("Default isDateSelected to false") {
myInterfaceWithNoMethods.isDateSelected(today) shouldBe false
}
should("Have a default implementation that doesn't throw an exception for onDateSelected") {
shouldNotThrowAny {
myInterfaceWithNoMethods.onDateSelected(today)
}
}
}
context("Selection State with confirm state change callback") {
var nextVetoResult = false
val initialSelection = LocalDate.of(1999, 10, 12)
val newSelection = initialSelection.plusDays(1)
val selectionState = DynamicSelectionState(
confirmSelectionChange = { nextVetoResult },
selection = listOf(initialSelection),
selectionMode = Multiple,
)
should("Not change the selection when change is vetoed") {
selectionState.onDateSelected(newSelection)
selectionState.selection shouldBe listOf(initialSelection)
}
should("Change the selection when change is not vetoed") {
nextVetoResult = true
selectionState.onDateSelected(newSelection)
selectionState.selection shouldBe listOf(initialSelection, newSelection)
}
}
})
@@ -0,0 +1,68 @@
package io.github.boguszpawlowski.composecalendar.week
import io.kotest.assertions.asClue
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.shouldBe
import java.time.LocalDate
internal class WeekTest : ShouldSpec({
val today = LocalDate.of(2000, 10, 2)
context("Secondary constructor") {
val week = Week(firstDay = today)
should("properly create an instance of week") {
week.asClue {
it.days.first() shouldBe today
it.days.last() shouldBe today.plusDays(6)
it.days.size shouldBe 7
}
}
}
context("Incrementing") {
val week = Week(firstDay = today).inc()
should("shift the days by 7") {
week.asClue {
it.days.first() shouldBe today.plusWeeks(1)
it.days.last() shouldBe today.plusDays(6).plusWeeks(1)
it.days.size shouldBe 7
}
}
}
context("Decrementing") {
val week = Week(firstDay = today).dec()
should("shift the days by -7") {
week.asClue {
it.days.first() shouldBe today.plusWeeks(-1)
it.days.last() shouldBe today.plusDays(6).plusWeeks(-1)
it.days.size shouldBe 7
}
}
}
context("Adding a week") {
val week = Week(firstDay = today).plusWeeks(7)
should("add correct number of days") {
week.asClue {
it.days.first() shouldBe today.plusWeeks(7)
it.days.last() shouldBe today.plusDays(6).plusWeeks(7)
it.days.size shouldBe 7
}
}
}
context("Constructing a week with invalid number of days") {
should("throw") {
shouldThrow<IllegalArgumentException> {
Week(List(18) { LocalDate.now() })
}
}
}
})
@@ -0,0 +1,166 @@
@file:Suppress("UnderscoresInNumericLiterals")
package io.github.boguszpawlowski.composecalendar.week
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.collections.shouldContainAll
import io.kotest.matchers.collections.shouldContainExactly
import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.shouldBe
import java.time.DayOfWeek.MONDAY
import java.time.DayOfWeek.SATURDAY
import java.time.DayOfWeek.SUNDAY
import java.time.LocalDate
import java.time.YearMonth
internal class WeekUtilTest : ShouldSpec({
val today = LocalDate.of(2020, 9, 15)
val month = YearMonth.of(2020, 9)
val previousMonth = YearMonth.of(2020, 8)
val nextMonth = YearMonth.of(2020, 10)
context("Extracting weeks without adjacent months") {
val includeAdjacentMonths = false
should("return days only from current month") {
val weeks = month.getWeeks(includeAdjacentMonths, MONDAY, today)
val days = weeks.flatMap { it.days }
days.map { it.date }.forEach {
it.isFromMonth(month) shouldBe true
it.isFromMonth(previousMonth) shouldBe false
it.isFromMonth(nextMonth) shouldBe false
}
}
should("return all days from the month") {
val weeks = month.getWeeks(includeAdjacentMonths, MONDAY, today)
val days = weeks.flatMap { it.days }.map { it.date }
val daysLength = month.lengthOfMonth()
val daysFromCurrentMonth = (1..daysLength).map { month.atDay(it) }
days shouldContainExactly daysFromCurrentMonth
}
should("return days properly split to weeks") {
val weeks = month.getWeeks(includeAdjacentMonths, MONDAY, today)
weeks[0].days shouldHaveSize 6
weeks[1].days shouldHaveSize 7
weeks[2].days shouldHaveSize 7
weeks[3].days shouldHaveSize 7
weeks[4].days shouldHaveSize 3
weeks shouldHaveSize 5
}
should("return days properly split to weeks when first day is Saturday") {
val weeks = month.getWeeks(includeAdjacentMonths, SATURDAY, today)
weeks[0].days shouldHaveSize 4
weeks[1].days shouldHaveSize 7
weeks[2].days shouldHaveSize 7
weeks[3].days shouldHaveSize 7
weeks[4].days shouldHaveSize 5
weeks shouldHaveSize 5
}
should("return days properly split to weeks when first day is Sunday ") {
val weeks = month.getWeeks(includeAdjacentMonths, SUNDAY, today)
weeks[0].days shouldHaveSize 5
weeks[1].days shouldHaveSize 7
weeks[2].days shouldHaveSize 7
weeks[3].days shouldHaveSize 7
weeks[4].days shouldHaveSize 4
weeks shouldHaveSize 5
}
}
context("Extracting weeks with adjacent months") {
val includeAdjacentMonths = true
should("return days from current and previous months") {
val weeks = month.getWeeks(includeAdjacentMonths, MONDAY, today)
val days = weeks.flatMap { it.days }
days.map { it.date }.forEachIndexed { index, day ->
when (index) {
0 -> {
day.isFromMonth(month) shouldBe false
day.isFromMonth(previousMonth) shouldBe true
day.isFromMonth(nextMonth) shouldBe false
}
in 1..30 -> {
day.isFromMonth(month) shouldBe true
day.isFromMonth(previousMonth) shouldBe false
day.isFromMonth(nextMonth) shouldBe false
}
else -> {
day.isFromMonth(month) shouldBe false
day.isFromMonth(previousMonth) shouldBe false
day.isFromMonth(nextMonth) shouldBe true
}
}
}
}
should("return all days from the month") {
val weeks = month.getWeeks(includeAdjacentMonths, MONDAY, today)
val days = weeks.flatMap { it.days }.map { it.date }
val daysLength = month.lengthOfMonth()
val endOffset = 6 - month.atEndOfMonth().dayOfWeek.ordinal
val startOffset = month.atDay(1).dayOfWeek.ordinal - 1
val daysFromCurrentMonth = (1..daysLength).map { month.atDay(it) }
val daysFromNextMonth = (1..endOffset).map { nextMonth.atDay(it) }
val daysFromPreviousMonth =
(previousMonth.lengthOfMonth() - startOffset..previousMonth.lengthOfMonth()).map {
previousMonth.atDay(it)
}
days shouldContainAll daysFromCurrentMonth + daysFromNextMonth + daysFromPreviousMonth
}
should("return days properly split to weeks") {
val weeks = month.getWeeks(includeAdjacentMonths, MONDAY, today)
weeks[0].days shouldHaveSize 7
weeks[1].days shouldHaveSize 7
weeks[2].days shouldHaveSize 7
weeks[3].days shouldHaveSize 7
weeks[4].days shouldHaveSize 7
weeks shouldHaveSize 5
}
should("return days properly split to weeks when first day is Sunday") {
val weeks = month.getWeeks(includeAdjacentMonths, SUNDAY, today)
weeks[0].days shouldHaveSize 7
weeks[1].days shouldHaveSize 7
weeks[2].days shouldHaveSize 7
weeks[3].days shouldHaveSize 7
weeks[4].days shouldHaveSize 7
weeks shouldHaveSize 5
}
should("return days properly split to weeks when first day is Saturday") {
val weeks = month.getWeeks(includeAdjacentMonths, SATURDAY, today)
weeks[0].days shouldHaveSize 7
weeks[1].days shouldHaveSize 7
weeks[2].days shouldHaveSize 7
weeks[3].days shouldHaveSize 7
weeks[4].days shouldHaveSize 7
weeks shouldHaveSize 5
}
}
})
private fun LocalDate.isFromMonth(yearMonth: YearMonth) =
month == yearMonth.month && year == yearMonth.year