Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add appStartTs to WidgetEnv. Make Timestamp a newtype #103

Merged
merged 9 commits into from
Mar 26, 2022
2 changes: 2 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- Support for breaking text lines at character boundaries ([PR #86](https://github.com/fjvallarino/monomer/pull/86)). Thanks @toku-sa-n!
- Read-only mode for `textField`, `numericField`, `dateField`, `timeField` and `textArea` ([PR #93](https://github.com/fjvallarino/monomer/pull/93)). Thanks @Dretch!
- The `scroll` widget now supports a `thumbMinSize` configuration option that allows setting a minimum thumb size ([PR #100](https://github.com/fjvallarino/monomer/pull/100)).
- New field `_weAppStartTs` in `WidgetEnv`, complementary to `_weTimestamp`, representing the time in milliseconds when the application started. Added utility function `currentTimeMs` that returns their sum with a polymorphic type ([PR #103](https://github.com/fjvallarino/monomer/pull/103)).

### Changed

Expand All @@ -21,6 +22,7 @@
in `handleEvent` to know when the model changed. Widgets that want to report model changes to its parent can
use `Report`/`RequestParent`; an example can be found in `ColorPicker` ([PR #71](https://github.com/fjvallarino/monomer/pull/71)).
- The `keystroke` widget now supports the `Backspace` key ([PR #74](https://github.com/fjvallarino/monomer/pull/74)).
- `Timestamp` is now a newtype. Enforce use of this type instead of `Int` when appropriate ([PR #103](https://github.com/fjvallarino/monomer/pull/103)).

### Renamed

Expand Down
2 changes: 1 addition & 1 deletion examples/todo/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ handleEvent wenv node model evt = case evt of
addNewTodo :: WidgetEnv s e -> TodoModel -> TodoModel
addNewTodo wenv model = newModel where
newTodo = model ^. activeTodo
& todoId .~ wenv ^. L.timestamp
& todoId .~ currentTimeMs wenv
newModel = model
& todos .~ (newTodo : model ^. todos)

Expand Down
2 changes: 1 addition & 1 deletion examples/todo/TodoTypes.hs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ data TodoStatus
deriving (Eq, Show, Enum)

data Todo = Todo {
_todoId :: Int,
_todoId :: Timestamp,
_todoType :: TodoType,
_status :: TodoStatus,
_description :: Text
Expand Down
7 changes: 4 additions & 3 deletions examples/tutorial/Tutorial03_LifeCycle.hs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import qualified Data.Text as T
import qualified Monomer.Lens as L

data ListItem = ListItem {
_ts :: Int,
_ts :: Timestamp,
_text :: Text
} deriving (Eq, Show)

Expand Down Expand Up @@ -59,7 +59,8 @@ buildUI wenv model = widgetTree where
keystroke [("Enter", AddItem)] $ hstack [
label "Description:",
spacer,
textField_ newItemText [placeholder "Write here!"], -- `nodeKey` "description",
textField_ newItemText [placeholder "Write here!"]
`nodeKey` "description",
spacer,
button "Add" AddItem
`styleBasic` [paddingH 5]
Expand Down Expand Up @@ -89,7 +90,7 @@ handleEvent wenv node model evt = case evt of
& items .~ removeIdx idx (model ^. items)]
_ -> []
where
newItem = ListItem (wenv ^. L.timestamp) (model ^. newItemText)
newItem = ListItem (currentTimeMs wenv) (model ^. newItemText)

removeIdx :: Int -> [a] -> [a]
removeIdx idx lst = part1 ++ drop 1 part2 where
Expand Down
8 changes: 8 additions & 0 deletions src/Monomer/Core/Util.hs
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,14 @@ isResizeAnyResult res = isResizeResult res || isResizeImmediateResult res
isMacOS :: WidgetEnv s e -> Bool
isMacOS wenv = _weOs wenv == "Mac OS X"

{-|
Returns the current time in milliseconds. Adds appStartTs and timestamp fields
from 'WidgetEnv' and converts the result to the expected 'Integral' type.
-}
currentTimeMs :: Integral a => WidgetEnv s e -> a
currentTimeMs wenv = fromIntegral ts where
ts = wenv ^. L.appStartTs + wenv ^. L.timestamp

-- | Returns a string description of a node and its children.
widgetTreeDesc :: Int -> WidgetNode s e -> String
widgetTreeDesc level node = desc where
Expand Down
27 changes: 21 additions & 6 deletions src/Monomer/Core/WidgetTypes.hs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Basic types and definitions for Widgets.
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# Language GeneralizedNewtypeDeriving #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE Strict #-}

Expand All @@ -24,7 +25,9 @@ import Data.Sequence (Seq)
import Data.String (IsString(..))
import Data.Text (Text)
import Data.Typeable (Typeable, typeOf)
import Data.Word (Word64)
import GHC.Generics
import TextShow

import qualified Data.Text as T

Expand All @@ -34,8 +37,15 @@ import Monomer.Core.ThemeTypes
import Monomer.Event.Types
import Monomer.Graphics.Types

-- | Time ellapsed since startup
type Timestamp = Int
{-|
Timestamp in milliseconds. Useful for representing the time of events, ellapsed
time since start/start time of the application and length of intervals.

It can be converted from/to other numeric types using the standard functions.
-}
newtype Timestamp = Timestamp {
unTimestamp :: Word64
} deriving (Show, Eq, Ord, Num, Enum, Bounded, Real, Integral, TextShow)

-- | Type constraints for a valid model
type WidgetModel s = Typeable s
Expand Down Expand Up @@ -94,8 +104,8 @@ Several WidgetRequests rely on this to find the destination of asynchronous
requests (tasks, clipboard, etc).
-}
data WidgetId = WidgetId {
_widTs :: Int, -- ^ The timestamp when the instance was created.
_widPath :: Path -- ^ The path at creation time.
_widTs :: Timestamp, -- ^ The timestamp when the instance was created.
_widPath :: Path -- ^ The path at creation time.
} deriving (Eq, Show, Ord, Generic)

instance Default WidgetId where
Expand Down Expand Up @@ -192,7 +202,7 @@ data WidgetRequest s e
| RenderOnce
-- | Useful if a widget requires periodic rendering. An optional maximum
-- number of frames can be provided.
| RenderEvery WidgetId Int (Maybe Int)
| RenderEvery WidgetId Timestamp (Maybe Int)
-- | Stops a previous periodic rendering request.
| RenderStop WidgetId
{-|
Expand Down Expand Up @@ -293,6 +303,8 @@ data WidgetEnv s e = WidgetEnv {
_weOs :: Text,
-- | Device pixel rate.
_weDpr :: Double,
-- | The timestamp in milliseconds when the application started.
_weAppStartTs :: Timestamp,
-- | Provides helper funtions for calculating text size.
_weFontManager :: FontManager,
-- | Returns the node info, and its parents', given a path from root.
Expand Down Expand Up @@ -325,7 +337,10 @@ data WidgetEnv s e = WidgetEnv {
_weModel :: s,
-- | The input status, mainly mouse and keyboard.
_weInputStatus :: InputStatus,
-- | The timestamp when this cycle started.
{-|
The timestamp in milliseconds when this event/message cycle started. This
value starts from zero each time the application is run.
-}
_weTimestamp :: Timestamp,
{-|
Whether the theme changed in this cycle. Should be considered when a widget
Expand Down
55 changes: 34 additions & 21 deletions src/Monomer/Main/Core.hs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import Data.Maybe
import Data.Map (Map)
import Data.List (foldl')
import Data.Text (Text)
import Data.Time
import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)
import Graphics.GL

import qualified Data.Map as Map
Expand Down Expand Up @@ -73,11 +75,11 @@ data MainLoopArgs sp e ep = MainLoopArgs {
_mlOS :: Text,
_mlRenderer :: Maybe Renderer,
_mlTheme :: Theme,
_mlAppStartTs :: Int,
_mlAppStartTs :: Timestamp,
_mlMaxFps :: Int,
_mlLatestRenderTs :: Int,
_mlFrameStartTs :: Int,
_mlFrameAccumTs :: Int,
_mlLatestRenderTs :: Timestamp,
_mlFrameStartTs :: Timestamp,
_mlFrameAccumTs :: Timestamp,
_mlFrameCount :: Int,
_mlExitEvents :: [e],
_mlWidgetRoot :: WidgetNode sp ep,
Expand Down Expand Up @@ -142,7 +144,7 @@ runAppLoop window glCtx channel widgetRoot config = do
let mainBtn = fromMaybe BtnLeft (_apcMainButton config)
let contextBtn = fromMaybe BtnRight (_apcContextButton config)

startTs <- fmap fromIntegral SDL.ticks
appStartTs <- getCurrentTimestamp
model <- use L.mainModel
os <- liftIO getPlatform
widgetSharedMVar <- liftIO $ newMVar Map.empty
Expand All @@ -154,6 +156,7 @@ runAppLoop window glCtx channel widgetRoot config = do
let wenv = WidgetEnv {
_weOs = os,
_weDpr = dpr,
_weAppStartTs = appStartTs,
_weFontManager = fontManager,
_weFindBranchByPath = const Seq.empty,
_weMainButton = mainBtn,
Expand All @@ -170,7 +173,7 @@ runAppLoop window glCtx channel widgetRoot config = do
_weMainBtnPress = Nothing,
_weModel = model,
_weInputStatus = def,
_weTimestamp = startTs,
_weTimestamp = 0,
_weThemeChanged = False,
_weInTopLayer = const True,
_weLayoutDirection = LayoutNone,
Expand All @@ -189,9 +192,9 @@ runAppLoop window glCtx channel widgetRoot config = do
_mlRenderer = renderer,
_mlTheme = theme,
_mlMaxFps = maxFps,
_mlAppStartTs = 0,
_mlAppStartTs = appStartTs,
_mlLatestRenderTs = 0,
_mlFrameStartTs = startTs,
_mlFrameStartTs = 0,
_mlFrameAccumTs = 0,
_mlFrameCount = 0,
_mlExitEvents = exitEvents,
Expand Down Expand Up @@ -219,7 +222,7 @@ mainLoop
mainLoop window fontManager config loopArgs = do
let MainLoopArgs{..} = loopArgs

startTicks <- fmap fromIntegral SDL.ticks
startTs <- getEllapsedTimestamp _mlAppStartTs
events <- SDL.pumpEvents >> SDL.pollEvents

windowSize <- use L.windowSize
Expand All @@ -237,7 +240,7 @@ mainLoop window fontManager config loopArgs = do
currWinSize <- liftIO $ getViewportSize window dpr

let Size rw rh = windowSize
let ts = startTicks - _mlFrameStartTs
let ts = startTs - _mlFrameStartTs
let eventsPayload = fmap SDL.eventPayload events
let quit = SDL.QuitEvent `elem` eventsPayload

Expand All @@ -264,6 +267,7 @@ mainLoop window fontManager config loopArgs = do
let wenv = WidgetEnv {
_weOs = _mlOS,
_weDpr = dpr,
_weAppStartTs = _mlAppStartTs,
_weFontManager = fontManager,
_weFindBranchByPath = findChildBranchByPath wenv _mlWidgetRoot,
_weMainButton = mainBtn,
Expand All @@ -280,7 +284,7 @@ mainLoop window fontManager config loopArgs = do
_weMainBtnPress = mainPress,
_weModel = currentModel,
_weInputStatus = inputStatus,
_weTimestamp = startTicks,
_weTimestamp = startTs,
_weThemeChanged = False,
_weInTopLayer = const True,
_weLayoutDirection = LayoutNone,
Expand All @@ -305,10 +309,10 @@ mainLoop window fontManager config loopArgs = do
handleResizeWidgets (seWenv, seRoot, Seq.empty)
else return (seWenv, seRoot, Seq.empty)

endTicks <- fmap fromIntegral SDL.ticks
endTs <- getEllapsedTimestamp _mlAppStartTs

-- Rendering
renderCurrentReq <- checkRenderCurrent startTicks _mlLatestRenderTs
renderCurrentReq <- checkRenderCurrent startTs _mlLatestRenderTs

let useRenderThread = fromMaybe True (_apcUseRenderThread config)
let renderEvent = any isActionEvent eventsPayload
Expand All @@ -328,14 +332,13 @@ mainLoop window fontManager config loopArgs = do

let fps = realToFrac _mlMaxFps
let frameLength = round (1000000 / fps)
let remainingMs = endTicks - startTicks
let tempDelay = abs (frameLength - remainingMs * 1000)
let remainingMs = endTs - startTs
let tempDelay = abs (frameLength - fromIntegral remainingMs * 1000)
let nextFrameDelay = min frameLength tempDelay
let latestRenderTs = if renderNeeded then startTicks else _mlLatestRenderTs
let latestRenderTs = if renderNeeded then startTs else _mlLatestRenderTs
let newLoopArgs = loopArgs {
_mlAppStartTs = _mlAppStartTs + ts,
_mlLatestRenderTs = latestRenderTs,
_mlFrameStartTs = startTicks,
_mlFrameStartTs = startTs,
_mlFrameAccumTs = if newSecond then 0 else _mlFrameAccumTs + ts,
_mlFrameCount = if newSecond then 0 else _mlFrameCount + 1,
_mlWidgetRoot = newRoot
Expand Down Expand Up @@ -469,7 +472,7 @@ watchWindowResize channel = do
atomically $ writeTChan channel (MsgResize newSize)
_ -> return ()

checkRenderCurrent :: (MonomerM s e m) => Int -> Int -> m Bool
checkRenderCurrent :: (MonomerM s e m) => Timestamp -> Timestamp -> m Bool
checkRenderCurrent currTs renderTs = do
renderCurrent <- use L.renderRequested
schedule <- use L.renderSchedule
Expand All @@ -479,14 +482,14 @@ checkRenderCurrent currTs renderTs = do
requiresRender = renderScheduleReq currTs renderTs
renderNext schedule = any requiresRender schedule

renderScheduleReq :: Int -> Int -> RenderSchedule -> Bool
renderScheduleReq :: Timestamp -> Timestamp -> RenderSchedule -> Bool
renderScheduleReq currTs renderTs schedule = required where
RenderSchedule _ start ms _ = schedule
stepCount = floor (fromIntegral (currTs - start) / fromIntegral ms)
stepTs = start + ms * stepCount
required = renderTs < stepTs

renderScheduleActive :: Int -> RenderSchedule -> Bool
renderScheduleActive :: Timestamp -> RenderSchedule -> Bool
renderScheduleActive currTs schedule = scheduleActive where
RenderSchedule _ start ms count = schedule
stepCount = floor (fromIntegral (currTs - start) / fromIntegral ms)
Expand All @@ -503,3 +506,13 @@ isWindowExposed eventsPayload = not status where
isMouseEntered :: [SDL.EventPayload] -> Bool
isMouseEntered eventsPayload = not status where
status = null [ e | e@SDL.WindowGainedMouseFocusEvent {} <- eventsPayload ]

getCurrentTimestamp :: MonadIO m => m Timestamp
getCurrentTimestamp = toMs <$> liftIO getCurrentTime
where
toMs = floor . (1e3 *) . nominalDiffTimeToSeconds . utcTimeToPOSIXSeconds

getEllapsedTimestamp :: MonadIO m => Timestamp -> m Timestamp
getEllapsedTimestamp start = do
ts <- getCurrentTimestamp
return (ts - start)
2 changes: 1 addition & 1 deletion src/Monomer/Main/Handlers.hs
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,7 @@ handleRenderOnce previousStep = do
handleRenderEvery
:: MonomerM s e m
=> WidgetId
-> Int
-> Timestamp
-> Maybe Int
-> HandlerStep s e
-> m (HandlerStep s e)
Expand Down
4 changes: 2 additions & 2 deletions src/Monomer/Main/Types.hs
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ be provided.
-}
data RenderSchedule = RenderSchedule {
_rsWidgetId :: WidgetId,
_rsStart :: Int,
_rsMs :: Int,
_rsStart :: Timestamp,
_rsMs :: Timestamp,
_rsRepeat :: Maybe Int
} deriving (Eq, Show, Generic)

Expand Down
8 changes: 4 additions & 4 deletions src/Monomer/Widgets/Animation/Fade.hs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ Configuration options for fade:
-}
data FadeCfg e = FadeCfg {
_fdcAutoStart :: Maybe Bool,
_fdcDuration :: Maybe Int,
_fdcDuration :: Maybe Timestamp,
_fdcOnFinished :: [e]
} deriving (Eq, Show)

Expand All @@ -80,7 +80,7 @@ instance CmbAutoStart (FadeCfg e) where
_fdcAutoStart = Just start
}

instance CmbDuration (FadeCfg e) Int where
instance CmbDuration (FadeCfg e) Timestamp where
duration dur = def {
_fdcDuration = Just dur
}
Expand All @@ -92,7 +92,7 @@ instance CmbOnFinished (FadeCfg e) e where

data FadeState = FadeState {
_fdsRunning :: Bool,
_fdsStartTs :: Int
_fdsStartTs :: Timestamp
} deriving (Eq, Show, Generic)

instance Default FadeState where
Expand Down Expand Up @@ -141,7 +141,7 @@ makeFade isFadeIn config state = widget where
autoStart = fromMaybe False (_fdcAutoStart config)
duration = fromMaybe 500 (_fdcDuration config)
period = 20
steps = duration `div` period
steps = fromIntegral $ duration `div` period

finishedReq node = delayedMessage node AnimationFinished duration
renderReq wenv node = req where
Expand Down
Loading