diff --git a/hatter.cabal b/hatter.cabal index 4b812c8..c5d180f 100644 --- a/hatter.cabal +++ b/hatter.cabal @@ -119,7 +119,8 @@ library containers < 1, bytestring < 1, transformers < 0.7, - time + time, + unwitch >= 3.0.0 && < 4 c-sources: cbits/android_stubs.c cbits/platform_log.c @@ -191,7 +192,8 @@ executable redraw-demo test build-depends: hatter, - text + text, + unwitch >= 3.0.0 && < 4 executable confetti-repro-demo import: common-options @@ -250,4 +252,5 @@ test-suite unit text, bytestring, directory, - filepath + filepath, + unwitch >= 3.0.0 && < 4 diff --git a/nix/cross-deps.nix b/nix/cross-deps.nix index 2621d4b..0a0570c 100644 --- a/nix/cross-deps.nix +++ b/nix/cross-deps.nix @@ -228,10 +228,18 @@ WRAPPER }); } else {}; + unwitchOverride = self: super: { + unwitch = self.callCabal2nix "unwitch" (builtins.fetchTarball { + url = "https://github.com/jappeace/unwitch/archive/2759bdd153f293e0e6524d0170e861e51302caa4.tar.gz"; + sha256 = "sha256:BGxZ1CQGIYP/gg/J9jua2/wSEH4qq7bW91qooNELUlI="; + }) {}; + }; + defaultOverrides = let common = pkgs.lib.composeManyExtensions [ vectorOverride + unwitchOverride thPackageDbOverride thIservOverride hatterOverride @@ -284,9 +292,13 @@ WRAPPER # so its .a and .conf are available for linking. hatterDep = if hatterSrc != null then [ crossHaskellPkgs.hatter ] else []; + # Hatter's own non-boot dependencies — must be collected so hatter's + # .conf can resolve them (collect-deps doesn't follow propagatedBuildInputs). + hatterOwnDeps = [ crossHaskellPkgs.unwitch ]; + in import ./collect-deps.nix { inherit pkgs ghc ghcPkgCmd; - deps = resolvedDeps ++ hatterDep; + deps = resolvedDeps ++ hatterDep ++ hatterOwnDeps; mainLibPnames = if hatterSrc != null then [ "hatter" ] else []; iservProxy = iservWrapper; } diff --git a/nix/hpkgs.nix b/nix/hpkgs.nix index 7c05658..5eec64a 100644 --- a/nix/hpkgs.nix +++ b/nix/hpkgs.nix @@ -9,5 +9,9 @@ pkgs.haskellPackages.override { # NB this is a bit silly because nix files are now considered for the build # bigger projects should consider putting haskell stuff in a subfolder hatter-project = hnew.callCabal2nix "hatter" ../. { }; + unwitch = hnew.callCabal2nix "unwitch" (builtins.fetchTarball { + url = "https://github.com/jappeace/unwitch/archive/2759bdd153f293e0e6524d0170e861e51302caa4.tar.gz"; + sha256 = "sha256:BGxZ1CQGIYP/gg/J9jua2/wSEH4qq7bW91qooNELUlI="; + }) {}; }; } diff --git a/nix/ios-deps.nix b/nix/ios-deps.nix index 48fc56e..9247fb8 100644 --- a/nix/ios-deps.nix +++ b/nix/ios-deps.nix @@ -18,8 +18,15 @@ let pkgs = import sources.nixpkgs {}; + unwitchOverride = self: super: { + unwitch = self.callCabal2nix "unwitch" (builtins.fetchTarball { + url = "https://github.com/jappeace/unwitch/archive/2759bdd153f293e0e6524d0170e861e51302caa4.tar.gz"; + sha256 = "sha256:BGxZ1CQGIYP/gg/J9jua2/wSEH4qq7bW91qooNELUlI="; + }) {}; + }; + nativeHaskellPkgs = pkgs.haskellPackages.override { - overrides = hpkgs; + overrides = pkgs.lib.composeExtensions unwitchOverride hpkgs; }; ghc = nativeHaskellPkgs.ghc; @@ -30,7 +37,11 @@ let haskellPkgs = nativeHaskellPkgs; }; + # Hatter's own non-boot dependencies — always included so mkIOSLib's + # raw GHC invocation can find them even without a consumer cabal file. + hatterOwnDeps = [ nativeHaskellPkgs.unwitch ]; + in import ./collect-deps.nix { inherit pkgs ghc ghcPkgCmd; - deps = resolvedDeps; + deps = resolvedDeps ++ hatterOwnDeps; } diff --git a/src/Hatter.hs b/src/Hatter.hs index 328c2ab..883f4dc 100644 --- a/src/Hatter.hs +++ b/src/Hatter.hs @@ -130,6 +130,7 @@ import Foreign.C.Types (CDouble(..), CInt(..)) import Foreign.Ptr (Ptr, castPtr, nullPtr) import Data.ByteString qualified as BS import Data.Word (Word8) +import Unwitch.Convert.CInt qualified as CInt import Hatter.Action ( Action(..) , OnChange(..) @@ -316,7 +317,7 @@ haskellOnUIEvent :: Ptr AppContext -> CInt -> IO () haskellOnUIEvent ctxPtr callbackId = withExceptionHandler ctxPtr $ do appCtx <- derefAppContext ctxPtr - dispatchEvent (acRenderState appCtx) (fromIntegral callbackId) + dispatchEvent (acRenderState appCtx) (CInt.toInt32 callbackId) renderView ctxPtr foreign export ccall haskellOnUIEvent :: Ptr AppContext -> CInt -> IO () @@ -332,7 +333,7 @@ haskellOnUITextChange ctxPtr callbackId cstr = withExceptionHandler ctxPtr $ do appCtx <- derefAppContext ctxPtr str <- peekCString cstr - dispatchTextEvent (acRenderState appCtx) (fromIntegral callbackId) (pack str) + dispatchTextEvent (acRenderState appCtx) (CInt.toInt32 callbackId) (pack str) renderView ctxPtr foreign export ccall haskellOnUITextChange :: Ptr AppContext -> CInt -> CString -> IO () @@ -452,7 +453,7 @@ haskellOnCameraResult ctxPtr requestId statusCode appCtx <- derefAppContext ctxPtr maybeImageData <- if imageDataPtr == nullPtr || imageDataLen <= 0 then pure Nothing - else Just <$> BS.packCStringLen (castPtr imageDataPtr, fromIntegral imageDataLen) + else Just <$> BS.packCStringLen (castPtr imageDataPtr, CInt.toInt imageDataLen) dispatchCameraResult (acCameraState appCtx) requestId statusCode maybeImageData width height @@ -467,7 +468,7 @@ haskellOnVideoFrame :: Ptr AppContext -> CInt haskellOnVideoFrame ctxPtr requestId frameDataPtr frameDataLen width height = withExceptionHandler ctxPtr $ do appCtx <- derefAppContext ctxPtr - frameBytes <- BS.packCStringLen (castPtr frameDataPtr, fromIntegral frameDataLen) + frameBytes <- BS.packCStringLen (castPtr frameDataPtr, CInt.toInt frameDataLen) dispatchVideoFrame (acCameraState appCtx) requestId frameBytes width height foreign export ccall haskellOnVideoFrame @@ -480,7 +481,7 @@ haskellOnAudioChunk :: Ptr AppContext -> CInt haskellOnAudioChunk ctxPtr requestId audioDataPtr audioDataLen = withExceptionHandler ctxPtr $ do appCtx <- derefAppContext ctxPtr - audioBytes <- BS.packCStringLen (castPtr audioDataPtr, fromIntegral audioDataLen) + audioBytes <- BS.packCStringLen (castPtr audioDataPtr, CInt.toInt audioDataLen) dispatchAudioChunk (acCameraState appCtx) requestId audioBytes foreign export ccall haskellOnAudioChunk @@ -508,7 +509,7 @@ haskellOnHttpResult ctxPtr requestId resultCode httpStatus maybeHeaders <- peekOptionalCString cHeaders responseBody <- if bodyPtr == nullPtr || bodyLen <= 0 then pure BS.empty - else BS.packCStringLen (castPtr bodyPtr, fromIntegral bodyLen) + else BS.packCStringLen (castPtr bodyPtr, CInt.toInt bodyLen) dispatchHttpResult (acHttpState appCtx) requestId resultCode httpStatus maybeHeaders responseBody diff --git a/src/Hatter/Action.hs b/src/Hatter/Action.hs index 5080e4a..41d9f6b 100644 --- a/src/Hatter/Action.hs +++ b/src/Hatter/Action.hs @@ -40,6 +40,7 @@ import Data.Int (Int32) import Data.IntMap.Strict (IntMap) import Data.IntMap.Strict qualified as IntMap import Data.Text (Text) +import Unwitch.Convert.Int32 qualified as Int32 -- | An opaque handle to a click / tap callback. -- Carries only an 'Int32' identifier, so it derives 'Eq' and 'Show'. @@ -86,7 +87,7 @@ newtype ActionM a = ActionM (ActionState -> IO a) createAction :: IO () -> ActionM Action createAction callback = ActionM $ \state -> do handleId <- readIORef (asNextId state) - modifyIORef' (asCallbacks state) (IntMap.insert (fromIntegral handleId) callback) + modifyIORef' (asCallbacks state) (IntMap.insert (Int32.toInt handleId) callback) modifyIORef' (asNextId state) (+ 1) pure (Action handleId) @@ -94,7 +95,7 @@ createAction callback = ActionM $ \state -> do createOnChange :: (Text -> IO ()) -> ActionM OnChange createOnChange callback = ActionM $ \state -> do handleId <- readIORef (asNextId state) - modifyIORef' (asTextCallbacks state) (IntMap.insert (fromIntegral handleId) callback) + modifyIORef' (asTextCallbacks state) (IntMap.insert (Int32.toInt handleId) callback) modifyIORef' (asNextId state) (+ 1) pure (OnChange handleId) @@ -107,11 +108,11 @@ runActionM state (ActionM f) = f state lookupAction :: ActionState -> Int32 -> IO (Maybe (IO ())) lookupAction state handleId = do callbacks <- readIORef (asCallbacks state) - pure (IntMap.lookup (fromIntegral handleId) callbacks) + pure (IntMap.lookup (Int32.toInt handleId) callbacks) -- | Look up a text-change callback by handle ID. -- Returns 'Nothing' if the ID is not registered. lookupTextAction :: ActionState -> Int32 -> IO (Maybe (Text -> IO ())) lookupTextAction state handleId = do callbacks <- readIORef (asTextCallbacks state) - pure (IntMap.lookup (fromIntegral handleId) callbacks) + pure (IntMap.lookup (Int32.toInt handleId) callbacks) diff --git a/src/Hatter/Animation.hs b/src/Hatter/Animation.hs index 029b7ff..db1085c 100644 --- a/src/Hatter/Animation.hs +++ b/src/Hatter/Animation.hs @@ -31,6 +31,7 @@ import Data.List (sortBy) import Data.Ord (comparing) import Data.Time.Clock (NominalDiffTime) import Foreign.Ptr (Ptr) +import Unwitch.Convert.Int32 qualified as Int32 import Hatter.Widget ( Keyframe(..) , WidgetStyle(..) @@ -86,7 +87,7 @@ registerTween animState nodeId keyframes duration = do , atNodeId = nodeId , atDuration = duration } - modifyIORef' (ansTweens animState) (IntMap.insert (fromIntegral nodeId) tween) + modifyIORef' (ansTweens animState) (IntMap.insert (Int32.toInt nodeId) tween) ensureLoopStarted animState -- | Start the platform animation loop if not already active. diff --git a/src/Hatter/AuthSession.hs b/src/Hatter/AuthSession.hs index b91511f..cc469b0 100644 --- a/src/Hatter/AuthSession.hs +++ b/src/Hatter/AuthSession.hs @@ -36,6 +36,8 @@ import Foreign.C.String (CString, withCString) import Foreign.C.Types (CInt(..)) import Foreign.Ptr (Ptr, nullPtr) import System.IO (hPutStrLn, stderr) +import Unwitch.Convert.CInt qualified as CInt +import Unwitch.Convert.Int32 qualified as Int32 -- | Result of an authentication session. data AuthSessionResult @@ -88,12 +90,12 @@ authSessionResultFromInt _ _ _ = Nothing startAuthSession :: AuthSessionState -> Text -> Text -> (AuthSessionResult -> IO ()) -> IO () startAuthSession authSessionState authUrl callbackScheme callback = do requestId <- readIORef (asNextId authSessionState) - modifyIORef' (asCallbacks authSessionState) (IntMap.insert (fromIntegral requestId) callback) + modifyIORef' (asCallbacks authSessionState) (IntMap.insert (Int32.toInt requestId) callback) writeIORef (asNextId authSessionState) (requestId + 1) ctx <- readIORef (asContextPtr authSessionState) withCString (Text.unpack authUrl) $ \cUrl -> withCString (Text.unpack callbackScheme) $ \cScheme -> - c_authSessionStart ctx (fromIntegral requestId) cUrl cScheme + c_authSessionStart ctx (Int32.toCInt requestId) cUrl cScheme -- | Dispatch an auth session result from the platform back to the -- registered Haskell callback. Removes the callback after firing. @@ -104,7 +106,7 @@ dispatchAuthSessionResult authSessionState requestId statusCode maybeRedirectUrl Nothing -> hPutStrLn stderr $ "dispatchAuthSessionResult: unknown status code " ++ show statusCode Just result -> do - let reqKey = fromIntegral requestId + let reqKey = CInt.toInt requestId callbacks <- readIORef (asCallbacks authSessionState) case IntMap.lookup reqKey callbacks of Just callback -> do diff --git a/src/Hatter/Ble.hs b/src/Hatter/Ble.hs index 492dd7d..9ade525 100644 --- a/src/Hatter/Ble.hs +++ b/src/Hatter/Ble.hs @@ -31,6 +31,7 @@ import Foreign.C.String (CString, peekCString) import Foreign.C.Types (CInt(..)) import Foreign.Ptr (Ptr, nullPtr) import System.IO (hPutStrLn, stderr) +import Unwitch.Convert.CInt qualified as CInt -- | Status of the platform's BLE adapter. data BleAdapterStatus @@ -136,7 +137,7 @@ dispatchBleScanResult bleState cName cAddr cRssi = do let scanResult = BleScanResult { bsrDeviceName = nameStr , bsrDeviceAddress = addrStr - , bsrRssi = fromIntegral cRssi + , bsrRssi = CInt.toInt cRssi } callback scanResult diff --git a/src/Hatter/BottomSheet.hs b/src/Hatter/BottomSheet.hs index 0bec8e5..a901f3f 100644 --- a/src/Hatter/BottomSheet.hs +++ b/src/Hatter/BottomSheet.hs @@ -32,6 +32,8 @@ import Foreign.C.String (CString, withCString) import Foreign.C.Types (CInt(..)) import Foreign.Ptr (Ptr, nullPtr) import System.IO (hPutStrLn, stderr) +import Unwitch.Convert.CInt qualified as CInt +import Unwitch.Convert.Int32 qualified as Int32 -- | Result of a bottom sheet interaction. data BottomSheetAction @@ -76,7 +78,7 @@ newBottomSheetState = do bottomSheetActionFromInt :: CInt -> Maybe BottomSheetAction bottomSheetActionFromInt (-1) = Just BottomSheetDismissed bottomSheetActionFromInt code - | code >= 0 = Just (BottomSheetItemSelected (fromIntegral code)) + | code >= 0 = Just (BottomSheetItemSelected (CInt.toInt32 code)) | otherwise = Nothing -- | Show a bottom sheet with the given configuration. Registers @@ -86,13 +88,13 @@ bottomSheetActionFromInt code showBottomSheet :: BottomSheetState -> BottomSheetConfig -> (BottomSheetAction -> IO ()) -> IO () showBottomSheet bottomSheetState config callback = do requestId <- readIORef (bssNextId bottomSheetState) - modifyIORef' (bssCallbacks bottomSheetState) (IntMap.insert (fromIntegral requestId) callback) + modifyIORef' (bssCallbacks bottomSheetState) (IntMap.insert (Int32.toInt requestId) callback) writeIORef (bssNextId bottomSheetState) (requestId + 1) ctx <- readIORef (bssContextPtr bottomSheetState) let joinedItems = Text.unpack (Text.intercalate "\n" (bscItems config)) withCString (Text.unpack (bscTitle config)) $ \cTitle -> withCString joinedItems $ \cItems -> - c_bottomSheetShow ctx (fromIntegral requestId) cTitle cItems + c_bottomSheetShow ctx (Int32.toCInt requestId) cTitle cItems -- | Dispatch a bottom sheet result from the platform back to the -- registered Haskell callback. Removes the callback after firing. @@ -103,7 +105,7 @@ dispatchBottomSheetResult bottomSheetState requestId actionCode = Nothing -> hPutStrLn stderr $ "dispatchBottomSheetResult: unknown action code " ++ show actionCode Just action -> do - let reqKey = fromIntegral requestId + let reqKey = CInt.toInt requestId callbacks <- readIORef (bssCallbacks bottomSheetState) case IntMap.lookup reqKey callbacks of Just callback -> do diff --git a/src/Hatter/Camera.hs b/src/Hatter/Camera.hs index ee99901..28d4c96 100644 --- a/src/Hatter/Camera.hs +++ b/src/Hatter/Camera.hs @@ -43,6 +43,8 @@ import Data.IntMap.Strict qualified as IntMap import Foreign.C.Types (CInt(..)) import Foreign.Ptr (Ptr, nullPtr) import System.IO (hPutStrLn, stderr) +import Unwitch.Convert.CInt qualified as CInt +import Unwitch.Convert.Int32 qualified as Int32 -- | Which camera to use. data CameraSource @@ -130,7 +132,7 @@ cameraStatusFromInt _ = Nothing startCameraSession :: CameraState -> CameraSource -> IO () startCameraSession cameraState source = do ctx <- readIORef (csContextPtr cameraState) - c_cameraStartSession ctx (fromIntegral (cameraSourceToInt source)) + c_cameraStartSession ctx (Int32.toCInt (cameraSourceToInt source)) -- | Stop the active camera session. -- Safe to call when no session is active (no-op). @@ -144,10 +146,10 @@ stopCameraSession _cameraState = capturePhoto :: CameraState -> (CameraResult -> IO ()) -> IO () capturePhoto cameraState callback = do requestId <- readIORef (csNextId cameraState) - modifyIORef' (csCallbacks cameraState) (IntMap.insert (fromIntegral requestId) callback) + modifyIORef' (csCallbacks cameraState) (IntMap.insert (Int32.toInt requestId) callback) writeIORef (csNextId cameraState) (requestId + 1) ctx <- readIORef (csContextPtr cameraState) - c_cameraCapturePhoto ctx (fromIntegral requestId) + c_cameraCapturePhoto ctx (Int32.toCInt requestId) -- | Start recording video. Registers three callbacks: -- @@ -163,13 +165,13 @@ startVideoCapture :: CameraState -> IO () startVideoCapture cameraState frameCallback audioCallback completionCallback = do requestId <- readIORef (csNextId cameraState) - let reqKey = fromIntegral requestId + let reqKey = Int32.toInt requestId modifyIORef' (csCallbacks cameraState) (IntMap.insert reqKey completionCallback) modifyIORef' (csFrameCallbacks cameraState) (IntMap.insert reqKey frameCallback) modifyIORef' (csAudioCallbacks cameraState) (IntMap.insert reqKey audioCallback) writeIORef (csNextId cameraState) (requestId + 1) ctx <- readIORef (csContextPtr cameraState) - c_cameraStartVideo ctx (fromIntegral requestId) + c_cameraStartVideo ctx (Int32.toCInt requestId) -- | Stop recording video. The callback registered by 'startVideoCapture' -- will be fired with a completion result. @@ -191,12 +193,12 @@ dispatchCameraResult cameraState requestId statusCode Nothing -> hPutStrLn stderr $ "dispatchCameraResult: unknown status code " ++ show statusCode Just status -> do - let reqKey = fromIntegral requestId + let reqKey = CInt.toInt requestId maybePicture = case status of CameraSuccess -> case maybeImageData of Just imageBytes -> Just Picture - { pictureWidth = fromIntegral imageWidth - , pictureHeight = fromIntegral imageHeight + { pictureWidth = CInt.toInt imageWidth + , pictureHeight = CInt.toInt imageHeight , pictureData = imageBytes } Nothing -> Nothing @@ -220,10 +222,10 @@ dispatchCameraResult cameraState requestId statusCode -- recording stops. dispatchVideoFrame :: CameraState -> CInt -> ByteString -> CInt -> CInt -> IO () dispatchVideoFrame cameraState requestId frameBytes frameWidth frameHeight = do - let reqKey = fromIntegral requestId + let reqKey = CInt.toInt requestId picture = Picture - { pictureWidth = fromIntegral frameWidth - , pictureHeight = fromIntegral frameHeight + { pictureWidth = CInt.toInt frameWidth + , pictureHeight = CInt.toInt frameHeight , pictureData = frameBytes } frameCallbacks <- readIORef (csFrameCallbacks cameraState) @@ -237,7 +239,7 @@ dispatchVideoFrame cameraState requestId frameBytes frameWidth frameHeight = do -- recording stops. dispatchAudioChunk :: CameraState -> CInt -> ByteString -> IO () dispatchAudioChunk cameraState requestId audioBytes = do - let reqKey = fromIntegral requestId + let reqKey = CInt.toInt requestId audioCallbacks <- readIORef (csAudioCallbacks cameraState) case IntMap.lookup reqKey audioCallbacks of Just callback -> callback audioBytes diff --git a/src/Hatter/Dialog.hs b/src/Hatter/Dialog.hs index be80d5e..a58ef3f 100644 --- a/src/Hatter/Dialog.hs +++ b/src/Hatter/Dialog.hs @@ -31,6 +31,8 @@ import Foreign.C.String (CString, withCString) import Foreign.C.Types (CInt(..)) import Foreign.Ptr (Ptr, nullPtr) import System.IO (hPutStrLn, stderr) +import Unwitch.Convert.CInt qualified as CInt +import Unwitch.Convert.Int32 qualified as Int32 -- | Which button was tapped, or whether the dialog was dismissed. data DialogAction @@ -91,7 +93,7 @@ dialogActionFromInt _ = Nothing showDialog :: DialogState -> DialogConfig -> (DialogAction -> IO ()) -> IO () showDialog dialogState config callback = do requestId <- readIORef (dsNextId dialogState) - modifyIORef' (dsCallbacks dialogState) (IntMap.insert (fromIntegral requestId) callback) + modifyIORef' (dsCallbacks dialogState) (IntMap.insert (Int32.toInt requestId) callback) writeIORef (dsNextId dialogState) (requestId + 1) ctx <- readIORef (dsContextPtr dialogState) withCString (Text.unpack (dcTitle config)) $ \cTitle -> @@ -99,7 +101,7 @@ showDialog dialogState config callback = do withCString (Text.unpack (dcButton1 config)) $ \cButton1 -> withOptionalCString (dcButton2 config) $ \cButton2 -> withOptionalCString (dcButton3 config) $ \cButton3 -> - c_dialogShow ctx (fromIntegral requestId) cTitle cMessage cButton1 cButton2 cButton3 + c_dialogShow ctx (Int32.toCInt requestId) cTitle cMessage cButton1 cButton2 cButton3 -- | Dispatch a dialog result from the platform back to the -- registered Haskell callback. Removes the callback after firing. @@ -110,7 +112,7 @@ dispatchDialogResult dialogState requestId actionCode = Nothing -> hPutStrLn stderr $ "dispatchDialogResult: unknown action code " ++ show actionCode Just action -> do - let reqKey = fromIntegral requestId + let reqKey = CInt.toInt requestId callbacks <- readIORef (dsCallbacks dialogState) case IntMap.lookup reqKey callbacks of Just callback -> do diff --git a/src/Hatter/Http.hs b/src/Hatter/Http.hs index b8c7054..e420152 100644 --- a/src/Hatter/Http.hs +++ b/src/Hatter/Http.hs @@ -43,6 +43,9 @@ import Foreign.C.String (CString, withCString) import Foreign.C.Types (CInt(..)) import Foreign.Ptr (Ptr, nullPtr) import System.IO (hPutStrLn, stderr) +import Unwitch.Convert.CInt qualified as CInt +import Unwitch.Convert.Int qualified as Int +import Unwitch.Convert.Int32 qualified as Int32 -- | HTTP request method. data HttpMethod @@ -128,7 +131,7 @@ parseHeaders headerText = performRequest :: HttpState -> HttpRequest -> (Either HttpError HttpResponse -> IO ()) -> IO () performRequest httpState request callback = do requestId <- readIORef (hsNextId httpState) - modifyIORef' (hsCallbacks httpState) (IntMap.insert (fromIntegral requestId) callback) + modifyIORef' (hsCallbacks httpState) (IntMap.insert (Int32.toInt requestId) callback) writeIORef (hsNextId httpState) (requestId + 1) ctx <- readIORef (hsContextPtr httpState) let methodInt = httpMethodToInt (hrMethod request) @@ -136,22 +139,22 @@ performRequest httpState request callback = do withCString (Text.unpack (hrUrl request)) $ \cUrl -> withCString headerStr $ \cHeaders -> BS.useAsCStringLen (hrBody request) $ \(cBody, bodyLen) -> - c_httpRequest ctx (fromIntegral requestId) methodInt - cUrl cHeaders cBody (fromIntegral bodyLen) + c_httpRequest ctx (Int32.toCInt requestId) methodInt + cUrl cHeaders cBody (maybe 0 id (Int.toCInt bodyLen)) -- | Dispatch an HTTP result from the platform back to the registered -- Haskell callback. Removes the callback after firing. -- Unknown request IDs or result codes are silently logged to stderr. dispatchHttpResult :: HttpState -> CInt -> CInt -> CInt -> Maybe Text -> ByteString -> IO () dispatchHttpResult httpState requestId resultCode httpStatus maybeHeaders responseBody = do - let reqKey = fromIntegral requestId + let reqKey = CInt.toInt requestId callbacks <- readIORef (hsCallbacks httpState) case IntMap.lookup reqKey callbacks of Just callback -> do modifyIORef' (hsCallbacks httpState) (IntMap.delete reqKey) let result = case resultCode of 0 -> Right HttpResponse - { hrStatusCode = fromIntegral httpStatus + { hrStatusCode = CInt.toInt httpStatus , hrRespHeaders = maybe [] parseHeaders maybeHeaders , hrRespBody = responseBody } diff --git a/src/Hatter/Permission.hs b/src/Hatter/Permission.hs index 73d49ac..dc18aa3 100644 --- a/src/Hatter/Permission.hs +++ b/src/Hatter/Permission.hs @@ -29,6 +29,8 @@ import Data.IntMap.Strict qualified as IntMap import Foreign.C.Types (CInt(..)) import Foreign.Ptr (Ptr, nullPtr) import System.IO (hPutStrLn, stderr) +import Unwitch.Convert.CInt qualified as CInt +import Unwitch.Convert.Int32 qualified as Int32 -- | Dangerous permissions that require runtime consent on mobile. data Permission @@ -98,10 +100,10 @@ permissionStatusFromInt _ = Nothing requestPermission :: PermissionState -> Permission -> (PermissionStatus -> IO ()) -> IO () requestPermission permissionState permission callback = do requestId <- readIORef (psNextId permissionState) - modifyIORef' (psCallbacks permissionState) (IntMap.insert (fromIntegral requestId) callback) + modifyIORef' (psCallbacks permissionState) (IntMap.insert (Int32.toInt requestId) callback) writeIORef (psNextId permissionState) (requestId + 1) ctx <- readIORef (psContextPtr permissionState) - c_permissionRequest ctx (permissionToInt permission) (fromIntegral requestId) + c_permissionRequest ctx (permissionToInt permission) (Int32.toCInt requestId) -- | Check whether a permission is currently granted (synchronous). checkPermission :: Permission -> IO PermissionStatus @@ -123,11 +125,11 @@ dispatchPermissionResult permissionState requestId statusCode = "dispatchPermissionResult: unknown status code " ++ show statusCode Just status -> do callbacks <- readIORef (psCallbacks permissionState) - case IntMap.lookup (fromIntegral requestId) callbacks of + case IntMap.lookup (CInt.toInt requestId) callbacks of Nothing -> hPutStrLn stderr $ "dispatchPermissionResult: unknown request ID " ++ show requestId Just callback -> do - modifyIORef' (psCallbacks permissionState) (IntMap.delete (fromIntegral requestId)) + modifyIORef' (psCallbacks permissionState) (IntMap.delete (CInt.toInt requestId)) callback status -- | FFI import: request a permission via the C bridge. diff --git a/src/Hatter/PlatformSignIn.hs b/src/Hatter/PlatformSignIn.hs index a878ea1..fa987b8 100644 --- a/src/Hatter/PlatformSignIn.hs +++ b/src/Hatter/PlatformSignIn.hs @@ -35,6 +35,8 @@ import Data.Text (Text) import Foreign.C.Types (CInt(..)) import Foreign.Ptr (Ptr, nullPtr) import System.IO (hPutStrLn, stderr) +import Unwitch.Convert.CInt qualified as CInt +import Unwitch.Convert.Int32 qualified as Int32 -- | Identity provider for platform sign-in. data SignInProvider @@ -126,10 +128,10 @@ signInResultFromInt _ _ _ _ _ _ = Nothing startPlatformSignIn :: PlatformSignInState -> SignInProvider -> (SignInResult -> IO ()) -> IO () startPlatformSignIn signInState provider callback = do requestId <- readIORef (psiNextId signInState) - modifyIORef' (psiCallbacks signInState) (IntMap.insert (fromIntegral requestId) callback) + modifyIORef' (psiCallbacks signInState) (IntMap.insert (Int32.toInt requestId) callback) writeIORef (psiNextId signInState) (requestId + 1) ctx <- readIORef (psiContextPtr signInState) - c_platformSignInStart ctx (fromIntegral requestId) (providerToInt provider) + c_platformSignInStart ctx (Int32.toCInt requestId) (providerToInt provider) -- | Dispatch a platform sign-in result from the platform back to the -- registered Haskell callback. Removes the callback after firing. @@ -140,7 +142,7 @@ dispatchPlatformSignInResult signInState requestId statusCode maybeToken maybeUs Nothing -> hPutStrLn stderr $ "dispatchPlatformSignInResult: unknown status code " ++ show statusCode Just result -> do - let reqKey = fromIntegral requestId + let reqKey = CInt.toInt requestId callbacks <- readIORef (psiCallbacks signInState) case IntMap.lookup reqKey callbacks of Just callback -> do diff --git a/src/Hatter/Render.hs b/src/Hatter/Render.hs index 80a2e89..e277241 100644 --- a/src/Hatter/Render.hs +++ b/src/Hatter/Render.hs @@ -31,6 +31,7 @@ import Data.Text (Text, pack) import Hatter.Action (Action(..), ActionState, OnChange(..), lookupAction, lookupTextAction) import Data.List (sortBy) import Data.Ord (comparing) +import Unwitch.Convert.Int32 qualified as Int32 import Hatter.Animation (AnimationState, registerTween) import Hatter.Widget (AnimatedConfig(..), ButtonConfig(..), FontConfig(..), ImageConfig(..), ImageSource(..), InputType(..), Keyframe(..), LayoutItem(..), LayoutSettings(..), MapViewConfig(..), ResourceName(..), ScaleType(..), TextAlignment(..), TextConfig(..), TextInputConfig(..), WebViewConfig(..), Widget(..), WidgetStyle(..), colorToHex, normalizeAnimated, resolveKeyAtIndex) @@ -192,7 +193,7 @@ createRenderedNode _animState widget@(TextInput config) = do nodeId <- Bridge.createNode Bridge.NodeTextInput Bridge.setStrProp nodeId Bridge.PropText (tiValue config) Bridge.setStrProp nodeId Bridge.PropHint (tiHint config) - Bridge.setNumProp nodeId Bridge.PropInputType (fromIntegral (inputTypeToInt (tiInputType config))) + Bridge.setNumProp nodeId Bridge.PropInputType (Int32.toDouble (inputTypeToInt (tiInputType config))) Bridge.setHandler nodeId Bridge.EventTextChange (onChangeId (tiOnChange config)) applyFontConfig nodeId (tiFontConfig config) when (tiAutoFocus config) $ @@ -437,7 +438,7 @@ diffRenderNode _animState (Just (RenderedLeaf (TextInput oldConfig) nodeId)) new else pure () if tiInputType oldConfig /= tiInputType newConfig then Bridge.setNumProp nodeId Bridge.PropInputType - (fromIntegral (inputTypeToInt (tiInputType newConfig))) + (Int32.toDouble (inputTypeToInt (tiInputType newConfig))) else pure () if onChangeId (tiOnChange oldConfig) /= onChangeId (tiOnChange newConfig) then Bridge.setHandler nodeId Bridge.EventTextChange diff --git a/src/Hatter/SecureStorage.hs b/src/Hatter/SecureStorage.hs index 2ddf8da..33b218d 100644 --- a/src/Hatter/SecureStorage.hs +++ b/src/Hatter/SecureStorage.hs @@ -32,6 +32,8 @@ import Foreign.C.String (CString, withCString) import Foreign.C.Types (CInt(..)) import Foreign.Ptr (Ptr, nullPtr) import System.IO (hPutStrLn, stderr) +import Unwitch.Convert.CInt qualified as CInt +import Unwitch.Convert.Int32 qualified as Int32 -- | Result of a secure storage operation. data SecureStorageStatus @@ -89,12 +91,12 @@ storageStatusFromInt _ = Nothing secureStorageWrite :: SecureStorageState -> Text -> Text -> (SecureStorageStatus -> IO ()) -> IO () secureStorageWrite storageState key value callback = do requestId <- readIORef (ssNextId storageState) - modifyIORef' (ssWriteCallbacks storageState) (IntMap.insert (fromIntegral requestId) callback) + modifyIORef' (ssWriteCallbacks storageState) (IntMap.insert (Int32.toInt requestId) callback) writeIORef (ssNextId storageState) (requestId + 1) ctx <- readIORef (ssContextPtr storageState) withCString (Text.unpack key) $ \cKey -> withCString (Text.unpack value) $ \cValue -> - c_secureStorageWrite ctx (fromIntegral requestId) cKey cValue + c_secureStorageWrite ctx (Int32.toCInt requestId) cKey cValue -- | Read a value from secure storage by key. Registers @callback@ and -- calls the C bridge. The callback receives the status and an optional @@ -102,22 +104,22 @@ secureStorageWrite storageState key value callback = do secureStorageRead :: SecureStorageState -> Text -> (SecureStorageStatus -> Maybe Text -> IO ()) -> IO () secureStorageRead storageState key callback = do requestId <- readIORef (ssNextId storageState) - modifyIORef' (ssReadCallbacks storageState) (IntMap.insert (fromIntegral requestId) callback) + modifyIORef' (ssReadCallbacks storageState) (IntMap.insert (Int32.toInt requestId) callback) writeIORef (ssNextId storageState) (requestId + 1) ctx <- readIORef (ssContextPtr storageState) withCString (Text.unpack key) $ \cKey -> - c_secureStorageRead ctx (fromIntegral requestId) cKey + c_secureStorageRead ctx (Int32.toCInt requestId) cKey -- | Delete a key from secure storage. Registers @callback@ and calls -- the C bridge. The callback fires when the platform responds. secureStorageDelete :: SecureStorageState -> Text -> (SecureStorageStatus -> IO ()) -> IO () secureStorageDelete storageState key callback = do requestId <- readIORef (ssNextId storageState) - modifyIORef' (ssDeleteCallbacks storageState) (IntMap.insert (fromIntegral requestId) callback) + modifyIORef' (ssDeleteCallbacks storageState) (IntMap.insert (Int32.toInt requestId) callback) writeIORef (ssNextId storageState) (requestId + 1) ctx <- readIORef (ssContextPtr storageState) withCString (Text.unpack key) $ \cKey -> - c_secureStorageDelete ctx (fromIntegral requestId) cKey + c_secureStorageDelete ctx (Int32.toCInt requestId) cKey -- | Dispatch a secure storage result from the platform back to the -- registered Haskell callback. Tries write callbacks first, then read, @@ -129,7 +131,7 @@ dispatchSecureStorageResult storageState requestId statusCode maybeValue = Nothing -> hPutStrLn stderr $ "dispatchSecureStorageResult: unknown status code " ++ show statusCode Just status -> do - let reqKey = fromIntegral requestId + let reqKey = CInt.toInt requestId -- Try write callbacks writeCallbacks <- readIORef (ssWriteCallbacks storageState) case IntMap.lookup reqKey writeCallbacks of diff --git a/src/Hatter/UIBridge.hs b/src/Hatter/UIBridge.hs index 04865d3..9ee6045 100644 --- a/src/Hatter/UIBridge.hs +++ b/src/Hatter/UIBridge.hs @@ -31,6 +31,9 @@ import Data.Word (Word8) import Foreign.C.String (CString, withCString) import Foreign.C.Types (CInt(..), CDouble(..)) import Foreign.Ptr (Ptr, castPtr) +import Unwitch.Convert.Int qualified as Int +import Unwitch.Convert.Int32 qualified as Int32 +import Unwitch.Convert.CInt qualified as CInt -- | Widget node types corresponding to @UI_NODE_*@ in @UIBridge.h@. data NodeType @@ -133,50 +136,50 @@ foreign import ccall "ui_clear" c_clear :: IO () -- | Create a native node of the given type. Returns an opaque node ID. createNode :: NodeType -> IO Int32 -createNode nt = fromIntegral <$> c_createNode (fromIntegral (nodeTypeToInt nt)) +createNode nt = CInt.toInt32 <$> c_createNode (Int32.toCInt (nodeTypeToInt nt)) -- | Set a string property on a node. setStrProp :: Int32 -> PropId -> Text -> IO () setStrProp nodeId propId value = withCString (unpack value) $ \cstr -> - c_setStrProp (fromIntegral nodeId) (fromIntegral (propIdToInt propId)) cstr + c_setStrProp (Int32.toCInt nodeId) (Int32.toCInt (propIdToInt propId)) cstr -- | Set a numeric property on a node. setNumProp :: Int32 -> PropId -> Double -> IO () setNumProp nodeId propId value = - c_setNumProp (fromIntegral nodeId) (fromIntegral (propIdToInt propId)) (realToFrac value) + c_setNumProp (Int32.toCInt nodeId) (Int32.toCInt (propIdToInt propId)) (realToFrac value) -- | Set raw image data (PNG/JPEG bytes) on a node. setImageData :: Int32 -> ByteString -> IO () setImageData nodeId imageBytes = BS.useAsCStringLen imageBytes $ \(ptr, len) -> - c_setImageData (fromIntegral nodeId) (castPtr ptr) (fromIntegral len) + c_setImageData (Int32.toCInt nodeId) (castPtr ptr) (maybe 0 id (Int.toCInt len)) -- | Register an event handler on a node. The @callbackId@ is looked up -- in the 'RenderState' callback registry when the event fires. setHandler :: Int32 -> EventType -> Int32 -> IO () setHandler nodeId eventType callbackId = - c_setHandler (fromIntegral nodeId) (fromIntegral (eventTypeToInt eventType)) (fromIntegral callbackId) + c_setHandler (Int32.toCInt nodeId) (Int32.toCInt (eventTypeToInt eventType)) (Int32.toCInt callbackId) -- | Add a child node to a parent container. addChild :: Int32 -> Int32 -> IO () addChild parentId childId = - c_addChild (fromIntegral parentId) (fromIntegral childId) + c_addChild (Int32.toCInt parentId) (Int32.toCInt childId) -- | Remove a child node from a parent container. removeChild :: Int32 -> Int32 -> IO () removeChild parentId childId = - c_removeChild (fromIntegral parentId) (fromIntegral childId) + c_removeChild (Int32.toCInt parentId) (Int32.toCInt childId) -- | Destroy a node and free its native resources. destroyNode :: Int32 -> IO () destroyNode nodeId = - c_destroyNode (fromIntegral nodeId) + c_destroyNode (Int32.toCInt nodeId) -- | Set a node as the root of the display. setRoot :: Int32 -> IO () setRoot nodeId = - c_setRoot (fromIntegral nodeId) + c_setRoot (Int32.toCInt nodeId) -- | Clear all nodes (called before re-render). clear :: IO () diff --git a/src/Hatter/Widget.hs b/src/Hatter/Widget.hs index 63d2fff..bef0870 100644 --- a/src/Hatter/Widget.hs +++ b/src/Hatter/Widget.hs @@ -73,6 +73,8 @@ import Data.Text qualified as Text import Data.Time.Clock (NominalDiffTime) import Data.Word (Word8) import Hatter.Action (Action, OnChange) +import Unwitch.Convert.Int qualified as Int +import Unwitch.Convert.Word8 qualified as Word8 -- | Font configuration for text-bearing widgets. -- Only 'Text', 'Button', and 'TextInput' can carry a 'FontConfig'. @@ -149,7 +151,7 @@ colorFromText raw = do if all isHexDigit hex then case hex of [r1, g1, b1] -> - let expand ch = let val = digitToInt ch in fromIntegral (val * 16 + val) + let expand ch = let val = digitToInt ch in maybe 0 id (Int.toWord8 (val * 16 + val)) in Just (Color (expand r1) (expand g1) (expand b1) 255) [r1, r2, g1, g2, b1, b2] -> Just (Color (hexByte r1 r2) (hexByte g1 g2) (hexByte b1 b2) 255) @@ -160,14 +162,14 @@ colorFromText raw = do -- | Convert two hex characters to a Word8. hexByte :: Char -> Char -> Word8 -hexByte high low = fromIntegral (digitToInt high * 16 + digitToInt low) +hexByte high low = maybe 0 id (Int.toWord8 (digitToInt high * 16 + digitToInt low)) -- | Convert a 'Color' to a hex string in @"#AARRGGBB"@ format for the C bridge. colorToHex :: Color -> Text colorToHex (Color r g b a) = Text.pack ('#' : toHexByte a ++ toHexByte r ++ toHexByte g ++ toHexByte b) where toHexByte :: Word8 -> String - toHexByte byte = [intToDigit (fromIntegral byte `div` 16), intToDigit (fromIntegral byte `mod` 16)] + toHexByte byte = [intToDigit (Word8.toInt byte `div` 16), intToDigit (Word8.toInt byte `mod` 16)] -- | Visual style overrides for a widget node. -- Font size is not here — it belongs in the config records of @@ -367,7 +369,7 @@ andThen first second = AnimatedConfig -- | Linearly interpolate a single 'Word8' channel. lerpWord8 :: Word8 -> Word8 -> Double -> Word8 lerpWord8 from to progress = - round (fromIntegral from + (fromIntegral to - fromIntegral from) * progress :: Double) + round (Word8.toDouble from + (Word8.toDouble to - Word8.toDouble from) * progress :: Double) -- | Interpolate between two colors by lerping each RGBA channel. interpolateColor :: Color -> Color -> Double -> Color diff --git a/test/RedrawDemoMain.hs b/test/RedrawDemoMain.hs index b87c46e..9f0f252 100644 --- a/test/RedrawDemoMain.hs +++ b/test/RedrawDemoMain.hs @@ -15,6 +15,7 @@ import Data.IORef (IORef, newIORef, readIORef, writeIORef) import Data.Text (pack) import Foreign.C.Types (CInt(..)) import Foreign.Ptr (Ptr) +import Unwitch.Convert.CInt qualified as CInt import Hatter ( MobileApp(..) , UserState(..) @@ -59,8 +60,8 @@ redrawView startedRef _userState = do -- Start C-level timer: 3 ticks, 3 seconds apart. -- The context was stored by redraw_store_ctx() in renderView. c_startPeriodicRedraw 3 3 - count <- fromIntegral <$> c_getPeriodicCounter - platformLog ("view rebuilt: count=" <> pack (show (count :: Int))) + count <- CInt.toInt <$> c_getPeriodicCounter + platformLog ("view rebuilt: count=" <> pack (show count)) pure $ column [Text TextConfig { tcLabel = "Count: " <> pack (show count) , tcFontConfig = Nothing diff --git a/test/Test/AppContextTests.hs b/test/Test/AppContextTests.hs index 012601d..9e39840 100644 --- a/test/Test/AppContextTests.hs +++ b/test/Test/AppContextTests.hs @@ -24,6 +24,7 @@ import Hatter , haskellOnLifecycle ) import Hatter.AppContext (AppContext(..), newAppContext, freeAppContext, derefAppContext) +import Unwitch.Convert.Int32 qualified as Int32 import Hatter.Lifecycle ( LifecycleEvent(..) , MobileContext(..) @@ -203,7 +204,7 @@ exceptionHandlerTests = testGroup "ExceptionHandler" -- First render to register the button callback haskellRenderUI ctxPtr -- Dispatch the button, which throws — handler overwrites view - haskellOnUIEvent ctxPtr (fromIntegral (actionId crashHandle)) + haskellOnUIEvent ctxPtr (Int32.toCInt (actionId crashHandle)) isError <- viewIsErrorWidget ctxPtr assertBool "view should be error widget after button callback exception" isError freeAppContext ctxPtr @@ -232,7 +233,7 @@ exceptionHandlerTests = testGroup "ExceptionHandler" -- Dispatch the dismiss action (pre-registered during newAppContext). appCtx <- derefAppContext ctxPtr let dismissId = actionId (acDismissAction appCtx) - haskellOnUIEvent ctxPtr (fromIntegral dismissId) + haskellOnUIEvent ctxPtr (Int32.toCInt dismissId) isStillError <- viewIsErrorWidget ctxPtr assertBool "should no longer show error widget after dismiss" (not isStillError) freeAppContext ctxPtr