1module DatePicker.UI.Month (MonthView, mkMonthView) where23import Data.Bool (bool)4import Data.List (find, findIndex)5import Data.List.NonEmpty qualified as NE6import Data.Maybe (fromJust)7import Data.Time.Calendar qualified as Cal8import Data.Time.Calendar.Month (Month, addMonths)9import Data.Time.LocalTime (LocalTime (LocalTime), TimeOfDay (TimeOfDay))10import DatePicker.UI (View (..))11import DatePicker.Util12import Graphics.Vty.Attributes qualified as Attr13import Graphics.Vty.Image qualified as I14import Graphics.Vty.Input.Events qualified as E1516data MonthView = MonthView17 { months :: [Month],18 curDay :: Cal.Day,19 numCols :: Int,20 movType :: Movement,21 weekOrd :: NE.NonEmpty Cal.DayOfWeek22 }2324instance View MonthView where25 draw = drawView26 process = processEvent2728mkMonthView ::29 [Month] ->30 Cal.Day ->31 Cal.DayOfWeek ->32 Bool ->33 Maybe MonthView34mkMonthView ms day firstWeekDay logicMove =35 let week = take 7 (enumFrom firstWeekDay)36 move = if logicMove then MLogical else MSpatial37 in if ms `hasDay'` day38 then Just (MonthView ms day 3 move $ NE.fromList week)39 else Nothing4041currentMonth :: MonthView -> Month42currentMonth MonthView {months = ms, curDay = d} =43 fromJust $ find (\m -> Cal.dayPeriod d == m) ms4445hasDay' :: [Month] -> Cal.Day -> Bool46hasDay' ms d = any (\m -> Cal.dayPeriod d == m) ms4748hasDay :: MonthView -> Cal.Day -> Bool49hasDay MonthView {months = ms} = hasDay' ms5051hasMonth :: MonthView -> Month -> Bool52hasMonth MonthView {months = ms} m =53 m `elem` ms5455-- TODO: Make this customizable to implement the cal(1) -m option.56firstDayOfWeek :: MonthView -> Bool57firstDayOfWeek mv@MonthView {curDay = d, weekOrd = ord} =58 Cal.dayOfWeek d == NE.head ord59 || Cal.periodFirstDay (currentMonth mv) == d6061-- TODO: Make this customizable to implement the cal(1) -m option.62lastDayOfWeek :: MonthView -> Bool63lastDayOfWeek mv@MonthView {curDay = d, weekOrd = ord} =64 Cal.dayOfWeek d == NE.last ord65 || Cal.periodLastDay (currentMonth mv) == d6667firstWeekDayOfMonth :: MonthView -> Bool68firstWeekDayOfMonth mv@MonthView {curDay = day} =69 firstWeekDay (currentMonth mv) (Cal.dayOfWeek day) == day70 where71 firstWeekDay :: Month -> Cal.DayOfWeek -> Cal.Day72 firstWeekDay m dw = Cal.firstDayOfWeekOnAfter dw (Cal.periodFirstDay m)7374lastWeekDayOfMonth :: MonthView -> Bool75lastWeekDayOfMonth mv@MonthView {curDay = day} =76 Just day == lastWeekDay (currentMonth mv) (Cal.dayOfWeek day)77 where78 lastWeekDay :: Month -> Cal.DayOfWeek -> Maybe Cal.Day79 lastWeekDay m dw = find ((==) dw . Cal.dayOfWeek) $ reverse (Cal.periodAllDays m)8081------------------------------------------------------------------------8283drawView :: MonthView -> I.Image84drawView MonthView {curDay = d, months = ms, numCols = cols, weekOrd = ord} =85 I.vertCat (map I.horizCat $ splitEvery cols (map drawView' ms))86 where87 drawView' :: Month -> I.Image88 drawView' m =89 let img = drawMonth m ord d90 in img I.<|> makePad 2 (I.imageHeight img) I.<-> makePad weekWidth 19192-- The return value specifies if the view has changed as a result93-- of processing the event, if so, 'drawView' needs to be invoked.94processEvent :: MonthView -> E.Event -> Either (Maybe MonthView) LocalTime95processEvent view@MonthView {curDay = day, movType = mov} (E.EvKey key _) =96 case key of97 E.KEnter -> Right $ LocalTime day (TimeOfDay 0 0 0)98 E.KUp -> Left $ moveCursor view mov PrevWeek99 E.KDown -> Left $ moveCursor view mov NextWeek100 E.KRight -> Left $ moveCursor view mov NextDay101 E.KLeft -> Left $ moveCursor view mov PrevDay102 _ -> Left Nothing103processEvent view (E.EvResize _ _) = Left $ Just view104processEvent _ _ = Left Nothing105106------------------------------------------------------------------------107108weekWidth :: Int109weekWidth = (2 * 7) + 6 -- +6 for spacing between weeks110111drawDay :: Cal.Day -> Bool -> I.Image112drawDay day curDay =113 let attr = if curDay then high else Attr.defAttr114 in I.string attr $ format "%_2e" day115 where116 high :: Attr.Attr117 high =118 Attr.defAttr119 `Attr.withBackColor` Attr.white120 `Attr.withForeColor` Attr.black121122drawWeeks :: Cal.Day -> NE.NonEmpty Week -> I.Image123drawWeeks curDay weeks =124 let w = NE.toList $ NE.map NE.toList weeks125 in I.vertCat $ zipWith zipFunc [0 ..] (map drawWeek w)126 where127 zipFunc :: Int -> I.Image -> I.Image128 zipFunc i = horizPad weekWidth (i == 0)129130 drawWeek :: [Cal.Day] -> I.Image131 drawWeek days =132 I.horizCat $133 addSep (map (\day -> drawDay day $ day == curDay) days)134135-- XXX: Unfortunately, 'MonthYear' does not implement 'FormatTime'.136drawMonthYear :: Month -> I.Image137drawMonthYear m =138 horizCenter weekWidth $139 I.string Attr.defAttr (format "%B %Y" m)140141drawMonth :: Month -> NE.NonEmpty Cal.DayOfWeek -> Cal.Day -> I.Image142drawMonth m ord curDay = drawMonthYear m I.<-> drawHeader ord I.<-> weeks143 where144 weeks :: I.Image145 weeks = drawWeeks curDay (monthWeeks m $ NE.head ord)146147drawHeader :: NE.NonEmpty Cal.DayOfWeek -> I.Image148drawHeader ord =149 let wdays = NE.map (format "%a") ord150 items = NE.map (I.string Attr.defAttr . shortenWeekDay) wdays151 in I.horizCat $ addSep (NE.toList items)152 where153 shortenWeekDay :: String -> String154 shortenWeekDay (f : s : _xs) = [f, s]155 shortenWeekDay s = s156157------------------------------------------------------------------------158159data Direction = NextDay | PrevDay | NextWeek | PrevWeek160 deriving (Eq, Show)161162data Movement = MLogical | MSpatial163 deriving (Eq, Show)164165moveCursor :: MonthView -> Movement -> Direction -> Maybe MonthView166moveCursor mv@MonthView {curDay = day, numCols = cols} mov dir167 | mov == MSpatial && lastDayOfWeek mv && dir == NextDay =168 moveSpatialHoriz mv 1 NE.head169 | mov == MSpatial && firstDayOfWeek mv && dir == PrevDay =170 moveSpatialHoriz mv (-1) NE.last171 | mov == MSpatial && firstWeekDayOfMonth mv && dir == PrevWeek =172 moveSpatialVert mv (cols * (-1)) NE.reverse173 | mov == MSpatial && lastWeekDayOfMonth mv && dir == NextWeek =174 moveSpatialVert mv cols id175 | otherwise =176 let newDay = moveLogical dir day177 in bool Nothing (Just mv {curDay = newDay}) (hasDay mv newDay)178179moveLogical :: Direction -> Cal.Day -> Cal.Day180moveLogical NextDay = Cal.addDays 1181moveLogical PrevDay = Cal.addDays (-1)182moveLogical NextWeek = addWeeks 1183moveLogical PrevWeek = addWeeks (-1)184185moveSpatial ::186 MonthView ->187 Int ->188 (Month -> Maybe Week) ->189 (Week -> Maybe Cal.Day) ->190 Maybe MonthView191moveSpatial mv inc selectWeek selectDay =192 let curMonth = currentMonth mv193 newMonth = addMonths (fromIntegral inc) curMonth194 in if hasMonth mv newMonth195 then (\d -> mv {curDay = d}) <$> (selectWeek newMonth >>= selectDay)196 else Nothing197198moveSpatialVert :: MonthView -> Int -> (Week -> Week) -> Maybe MonthView199moveSpatialVert mv@MonthView {curDay = day} inc proc =200 let dayOfWeek = Cal.dayOfWeek day201 in moveSpatial202 mv203 inc204 (Just . proc . NE.fromList . Cal.periodAllDays)205 (find ((==) dayOfWeek . Cal.dayOfWeek))206207moveSpatialHoriz :: MonthView -> Int -> (Week -> Cal.Day) -> Maybe MonthView208moveSpatialHoriz mv@MonthView {curDay = day, weekOrd = ord} inc select =209 moveSpatial mv inc (\m -> monthWeeks m f !? weekOfMonth day) (Just . select)210 where211 f :: Cal.DayOfWeek212 f = NE.head ord213214 weekOfMonth :: Cal.Day -> Int215 weekOfMonth d =216 let weeks = monthWeeks (Cal.dayPeriod d) f217 in fromJust $ findIndex (elem d) (NE.toList weeks)218219{- ORMOLU_DISABLE -}220-- From https://github.com/ghc/ghc/commit/d53f6f4d98aabd6f5b28fb110db1da0f6db70a06221(!?) :: NE.NonEmpty a -> Int -> Maybe a222xs !? n223 | n < 0 = Nothing224 | otherwise = foldr (\x r k -> case k of225 0 -> Just x226 _ -> r (k-1)) (const Nothing) xs n227228infixl 9 !?229{- ORMOLU_ENABLE -}