packages feed

brillo-examples-2.0.0: picture/Cursors/Main.hs

{-# LANGUAGE OverloadedStrings #-}

{-| Cursor demo showing all available cursor shapes.
  Hover over each button to see the corresponding cursor.
-}
module Main where

import Brillo
import Brillo.Interface.IO.Interact
import Control.Monad (foldM)
import Data.IORef (IORef, newIORef, readIORef, writeIORef)
import Data.List (isPrefixOf)
import Data.Maybe (listToMaybe)
import Data.Text qualified as T
import System.Directory (doesFileExist)
import System.Environment (getArgs)
import System.Exit (exitFailure)
import System.IO (hPutStrLn, stderr)


main :: IO ()
main = do
  args <- getArgs
  let fontArg = listToMaybe [a | a <- args, not ("--" `isPrefixOf` a)]
  fontPath <- resolveFont fontArg

  -- Create an IORef to store the controller once we get it
  controllerRef <- newIORef Nothing

  let initialState =
        AppState
          { fontPath = fontPath
          , mousePos = (0, 0)
          , hoveredCursor = Nothing
          }

  interactIO
    (InWindow "Cursor Demo" (600, 500) (100, 100))
    (greyN 0.95)
    initialState
    (pure . renderScene)
    (handleEvent controllerRef)
    (writeIORef controllerRef . Just)


-- | Application state
data AppState
  = AppState
  { fontPath :: FilePath
  , mousePos :: Point
  , hoveredCursor :: Maybe CursorShape
  }


-- | Button definition linking to a cursor shape
data CursorButton
  = CursorButton
  { cbShape :: CursorShape
  , cbLabel :: T.Text
  , cbDescription :: T.Text
  , cbX :: Float
  , cbY :: Float
  , cbWidth :: Float
  , cbHeight :: Float
  }


-- | Define buttons for each cursor type
cursorButtons :: [CursorButton]
cursorButtons =
  [ CursorButton CursorArrow "Arrow" "Default pointer" (-200) 80 180 60
  , CursorButton CursorHand "Hand" "Clickable items" (50) 80 180 60
  , CursorButton CursorIBeam "I-Beam" "Text selection" (-200) 0 180 60
  , CursorButton CursorCrosshair "Crosshair" "Precision select" (50) 0 180 60
  , CursorButton CursorResizeH "Resize H" "Horizontal resize" (-200) (-80) 180 60
  , CursorButton CursorResizeV "Resize V" "Vertical resize" (50) (-80) 180 60
  , CursorButton CursorHidden "Hidden" "No cursor" (-75) (-160) 180 60
  ]


-- | Check if a point is inside a button
pointInButton :: Point -> CursorButton -> Bool
pointInButton (x, y) btn =
  let x1 = cbX btn
      y1 = cbY btn
      x2 = x1 + cbWidth btn
      y2 = y1 + cbHeight btn
  in  x >= x1 && x <= x2 && y >= y1 && y <= y2


-- | Find which cursor button (if any) is at the given point
findButtonAt :: Point -> [CursorButton] -> Maybe CursorButton
findButtonAt pt btns =
  case filter (pointInButton pt) btns of
    (btn : _) -> Just btn
    [] -> Nothing


-- | Render the entire scene
renderScene :: AppState -> Picture
renderScene state =
  Pictures
    [ -- Header
      Translate 0 200 $ renderHeader (fontPath state)
    , -- Buttons
      Pictures $ map (renderButton state) cursorButtons
    , -- Status bar
      Translate 0 (-200) $ renderStatusBar state
    ]


-- | Render the header
renderHeader :: FilePath -> Picture
renderHeader font =
  Pictures
    [ Color (makeColor 0.2 0.5 0.7 1.0) $ rectangleSolid 600 60
    , Translate (-250) (-12) $
        Color white $
          TrueTypeText font 32 "Cursor Shape Demo"
    ]


-- | Render a cursor button
renderButton :: AppState -> CursorButton -> Picture
renderButton state btn =
  let isHovered = hoveredCursor state == Just (cbShape btn)

      bgColor
        | isHovered = makeColor 0.3 0.6 0.9 1.0
        | otherwise = makeColor 0.6 0.6 0.6 1.0

      textColor
        | isHovered = white
        | otherwise = greyN 0.95

      cx = cbX btn + cbWidth btn / 2
      cy = cbY btn + cbHeight btn / 2
  in  Translate cx cy $
        Pictures
          [ -- Background
            Color bgColor $ rectangleSolid (cbWidth btn) (cbHeight btn)
          , -- Border
            Color (greyN 0.3) $ rectangleWire (cbWidth btn) (cbHeight btn)
          , -- Label
            Translate (-80) 5 $
              Color textColor $
                TrueTypeText (fontPath state) 22 (cbLabel btn)
          , -- Description
            Translate (-80) (-18) $
              Color (if isHovered then greyN 0.9 else greyN 0.75) $
                TrueTypeText (fontPath state) 14 (cbDescription btn)
          ]


-- | Render the status bar
renderStatusBar :: AppState -> Picture
renderStatusBar state =
  let cursorText = case hoveredCursor state of
        Nothing -> "Hover over a button to change cursor"
        Just shape -> "Current cursor: " <> T.pack (show shape)
  in  Pictures
        [ Color (greyN 0.85) $ rectangleSolid 600 50
        , Translate (-270) (-8) $
            Color (greyN 0.3) $
              TrueTypeText (fontPath state) 18 cursorText
        ]


-- | Handle input events
handleEvent :: IORef (Maybe Controller) -> Event -> AppState -> IO AppState
handleEvent controllerRef event state =
  case event of
    EventMotion pos -> do
      let mBtn = findButtonAt pos cursorButtons
          newHovered = cbShape <$> mBtn

      -- Update cursor when hovering changes
      mCtrl <- readIORef controllerRef
      case (mCtrl, newHovered) of
        (Just ctrl, Just shape) -> controllerSetCursor ctrl shape
        (Just ctrl, Nothing) -> controllerSetCursor ctrl CursorArrow
        _ -> pure ()

      pure $
        state
          { mousePos = pos
          , hoveredCursor = newHovered
          }
    _ -> pure state


-- | Font resolution logic
resolveFont :: Maybe FilePath -> IO FilePath
resolveFont (Just fp) = ensureFont fp
resolveFont Nothing = do
  found <- foldM pick Nothing candidateFonts
  case found of
    Just fp -> pure fp
    Nothing -> do
      hPutStrLn stderr $
        unlines
          [ "brillo-cursors: unable to locate a TrueType font."
          , "Tried the following paths:"
          , unlines (map ("  - " <>) candidateFonts)
          , "Pass a font explicitly: stack run brillo-cursors -- /path/to/font.ttf"
          ]
      exitFailure
  where
    pick acc candidate =
      case acc of
        Just _ -> pure acc
        Nothing -> do
          exists <- doesFileExist candidate
          pure $ if exists then Just candidate else Nothing


ensureFont :: FilePath -> IO FilePath
ensureFont fp = do
  exists <- doesFileExist fp
  if exists
    then pure fp
    else do
      hPutStrLn stderr $ "brillo-cursors: font file not found: " <> fp
      exitFailure


candidateFonts :: [FilePath]
candidateFonts =
  [ "/System/Library/Fonts/Supplemental/Arial.ttf"
  , "/Library/Fonts/Arial.ttf"
  , "/System/Library/Fonts/Supplemental/Helvetica.ttc"
  , "/System/Library/Fonts/Supplemental/Tahoma.ttf"
  , "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
  , "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf"
  , "/usr/share/fonts/truetype/freefont/FreeSans.ttf"
  , "C:\\Windows\\Fonts\\arial.ttf"
  , "C:\\Windows\\Fonts\\segoeui.ttf"
  ]