packages feed

taffybar-4.1.2: src/System/Taffybar/Widget/BatteryDonut.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards   #-}
-----------------------------------------------------------------------------
-- |
-- Module      : System.Taffybar.Widget.BatteryDonut
-- Copyright   : (c) Ivan A. Malison
-- License     : BSD3-style (see LICENSE)
--
-- Maintainer  : Ivan A. Malison
-- Stability   : unstable
-- Portability : unportable
--
-- A battery widget that draws a donut\/arc using Cairo to visualize battery
-- charge level. The arc color changes based on configurable charge thresholds.
-----------------------------------------------------------------------------
module System.Taffybar.Widget.BatteryDonut
  ( batteryDonutNew
  , batteryDonutNewWith
  , batteryDonutLabelNew
  , batteryDonutLabelNewWith
  , BatteryDonutConfig(..)
  , defaultBatteryDonutConfig
  ) where

import           Control.Monad
import           Control.Monad.IO.Class
import           Control.Monad.Trans.Reader
import           Data.Default (Default(..))
import           Data.Int (Int32)
import           Data.IORef
import qualified GI.Cairo.Render as C
import           GI.Cairo.Render.Connector (renderWithContext)
import           GI.Gtk as Gtk
import           System.Taffybar.Context
import           System.Taffybar.Information.Battery
import           System.Taffybar.Util
import           System.Taffybar.Widget.Generic.ChannelWidget
import           System.Taffybar.Widget.Battery (textBatteryNew)
import           System.Taffybar.Widget.Util (widgetSetClassGI,
                                               addClassIfMissing, removeClassIfPresent,
                                               buildIconLabelBox)
import qualified System.Taffybar.Widget.Util as WU

-- | Configuration for the donut/arc battery widget.
data BatteryDonutConfig = BatteryDonutConfig
  { donutHighColor :: (Double, Double, Double)
    -- ^ Color when charge is high (default: green)
  , donutMediumColor :: (Double, Double, Double)
    -- ^ Color when charge is medium (default: yellow)
  , donutLowColor :: (Double, Double, Double)
    -- ^ Color when charge is low (default: orange)
  , donutCriticalColor :: (Double, Double, Double)
    -- ^ Color when charge is critical (default: red)
  , donutBackgroundColor :: (Double, Double, Double)
    -- ^ Background ring color (default: dim gray)
  , donutHighThreshold :: Double
    -- ^ Percentage above which color is "high" (default: 60)
  , donutMediumThreshold :: Double
    -- ^ Percentage above which color is "medium" (default: 30)
  , donutCriticalThreshold :: Double
    -- ^ Percentage below which color is "critical" (default: 10)
  , donutSize :: Int32
    -- ^ Requested widget size in pixels (default: 24)
  , donutChargingColor :: Maybe (Double, Double, Double)
    -- ^ Optional override color when charging (default: Nothing)
  , donutLabelFormat :: String
    -- ^ Format string for the text label in the combined donut+label variant.
    -- Uses the same template syntax as 'textBatteryNew': @$percentage$@,
    -- @$time$@, and @$status$@ are replaced with the corresponding values.
    -- (default: @"$percentage$%"@)
  }

-- | Default donut battery configuration.
defaultBatteryDonutConfig :: BatteryDonutConfig
defaultBatteryDonutConfig = BatteryDonutConfig
  { donutHighColor = (0.3, 0.85, 0.3)
  , donutMediumColor = (0.95, 0.85, 0.2)
  , donutLowColor = (0.95, 0.5, 0.1)
  , donutCriticalColor = (0.9, 0.2, 0.2)
  , donutBackgroundColor = (0.3, 0.3, 0.3)
  , donutHighThreshold = 60
  , donutMediumThreshold = 30
  , donutCriticalThreshold = 10
  , donutSize = 24
  , donutChargingColor = Nothing
  , donutLabelFormat = "$percentage$%"
  }

instance Default BatteryDonutConfig where
  def = defaultBatteryDonutConfig

-- | Select the arc color based on battery percentage and charging state.
donutColorForLevel :: BatteryDonutConfig -> Double -> BatteryState -> (Double, Double, Double)
donutColorForLevel BatteryDonutConfig{..} pct state =
  case (state, donutChargingColor) of
    (BatteryStateCharging, Just color) -> color
    _ | pct >= donutHighThreshold     -> donutHighColor
      | pct >= donutMediumThreshold   -> donutMediumColor
      | pct >= donutCriticalThreshold -> donutLowColor
      | otherwise                     -> donutCriticalColor

-- | Render the donut arc for the battery.
renderDonut :: BatteryDonutConfig -> Double -> BatteryState -> Int -> Int -> C.Render ()
renderDonut cfg pct state w h = do
  let size = fromIntegral (min w h) :: Double
      cx = fromIntegral w / 2
      cy = fromIntegral h / 2
      lineW = size * 0.22
      radius = (size - lineW) / 2
      startAngle = -(pi / 2)
      fullAngle = 2 * pi
      fraction = pct / 100
      endAngle = startAngle + fullAngle * fraction
      (bgR, bgG, bgB) = donutBackgroundColor cfg
      (fgR, fgG, fgB) = donutColorForLevel cfg pct state

  -- Draw background ring
  C.setLineCap C.LineCapRound
  C.setLineWidth lineW
  C.setSourceRGB bgR bgG bgB
  C.arc cx cy radius 0 fullAngle
  C.stroke

  -- Draw foreground arc (only if there is something to draw)
  when (fraction > 0.001) $ do
    C.setSourceRGB fgR fgG fgB
    C.arc cx cy radius startAngle endAngle
    C.stroke

-- | Set CSS classes on the donut widget based on battery state.
setDonutBatteryClasses ::
  (Gtk.IsWidget w, MonadIO m) => BatteryDonutConfig -> w -> BatteryInfo -> m ()
setDonutBatteryClasses cfg widget info = do
  case batteryState info of
    BatteryStateCharging -> addClassIfMissing "charging" widget >>
                            removeClassIfPresent "discharging" widget
    BatteryStateDischarging -> addClassIfMissing "discharging" widget >>
                               removeClassIfPresent "charging" widget
    _ -> removeClassIfPresent "charging" widget >>
         removeClassIfPresent "discharging" widget

  classIf "high" $ percentage >= donutHighThreshold cfg
  classIf "medium" $ percentage >= donutMediumThreshold cfg &&
                     percentage < donutHighThreshold cfg
  classIf "low" $ percentage < donutMediumThreshold cfg &&
                  percentage >= donutCriticalThreshold cfg
  classIf "critical" $ percentage < donutCriticalThreshold cfg
  where percentage = batteryPercentage info
        classIf klass condition =
          if condition
          then addClassIfMissing klass widget
          else removeClassIfPresent klass widget

-- | Create a donut/arc battery widget with the default configuration.
batteryDonutNew :: TaffyIO Widget
batteryDonutNew = batteryDonutNewWith def

-- | Create a donut/arc battery widget with a custom configuration.
batteryDonutNewWith :: BatteryDonutConfig -> TaffyIO Widget
batteryDonutNewWith cfg = do
  chan <- getDisplayBatteryChan
  ctx <- ask
  liftIO $ do
    drawArea <- drawingAreaNew
    widgetSetSizeRequest drawArea (donutSize cfg) (donutSize cfg)

    pctRef <- newIORef 0.0
    stateRef <- newIORef BatteryStateUnknown

    _ <- widgetSetClassGI drawArea "battery-donut"

    _ <- onWidgetDraw drawArea $ \cairoCtx -> do
      (w, h) <- WU.widgetGetAllocatedSize drawArea
      pct <- readIORef pctRef
      bState <- readIORef stateRef
      renderWithContext (renderDonut cfg pct bState w h) cairoCtx
      return True

    let updateWidget info = postGUIASync $ do
          writeIORef pctRef (batteryPercentage info)
          writeIORef stateRef (batteryState info)
          setDonutBatteryClasses cfg drawArea info
          widgetQueueDraw drawArea

    void $ onWidgetRealize drawArea $
      runReaderT getDisplayBatteryInfo ctx >>= updateWidget

    toWidget =<< channelWidgetNew drawArea chan updateWidget

-- | Create a combined donut icon + text label battery widget with the default
-- configuration.
batteryDonutLabelNew :: TaffyIO Widget
batteryDonutLabelNew = batteryDonutLabelNewWith def

-- | Create a combined donut icon + text label battery widget with a custom
-- configuration.
batteryDonutLabelNewWith :: BatteryDonutConfig -> TaffyIO Widget
batteryDonutLabelNewWith config = do
  iconWidget <- batteryDonutNewWith config
  labelWidget <- textBatteryNew (donutLabelFormat config)
  liftIO $ buildIconLabelBox iconWidget labelWidget