packages feed

monomer-1.2.0.0: src/Monomer/Widgets/Containers/Scroll.hs

{-|
Module      : Monomer.Widgets.Containers.Scroll
Copyright   : (c) 2018 Francisco Vallarino
License     : BSD-3-Clause (see the LICENSE file)
Maintainer  : fjvallarino@gmail.com
Stability   : experimental
Portability : non-portable

Scroll container of a single node. Assigns all the space the inner node requests
but limits itself to what its parent assigns. It allows navigating the content
of the inner node with the scroll bars. It also supports automatic focus
following.

Messages:

- 'ScrollTo': Causes the scroll to update its handles to ensure rect is visible.
- 'ScrollReset': Sets both handle positions to zero.
-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE StrictData #-}

module Monomer.Widgets.Containers.Scroll (
  -- * Configuration
  ScrollCfg,
  ScrollMessage(..),
  scrollOverlay,
  scrollOverlay_,
  scrollFwdStyle,
  scrollFwdDefault,
  scrollInvisible,
  scrollInvisible_,
  scrollFollowFocus,
  scrollFollowFocus_,
  scrollStyle,
  -- * Constructors
  scroll,
  scroll_,
  hscroll,
  hscroll_,
  vscroll,
  vscroll_
) where

import Control.Applicative ((<|>))
import Control.Lens (ALens', (&), (^.), (.~), (^?), (^?!), (<>~), (%~), _Just, cloneLens, ix)
import Control.Monad
import Data.Default
import Data.Maybe
import Data.Typeable (cast)
import GHC.Generics

import qualified Data.Sequence as Seq

import Monomer.Helper
import Monomer.Widgets.Container

import qualified Monomer.Lens as L

data ScrollType
  = ScrollH
  | ScrollV
  | ScrollBoth
  deriving (Eq, Show)

data ActiveBar
  = HBar
  | VBar
  deriving (Eq, Show, Generic)

{-|
Configuration options for scroll:

- 'wheelRate': rate at which wheel movement causes scrolling.
- 'barColor': the color of the bar (container of the thumb).
- 'barHoverColor': the color of the bar when mouse is on top.
- 'barWidth': the width of the bar.
- 'thumbColor': the color of the thumb.
- 'thumbHoverColor': the color of the thumb when mouse is on top.
- 'thumbWidth': the width of the thumb.
- 'thumbRadius': the radius of the corners of the thumb.
- 'scrollOverlay': whether scroll bar should be on top of content or by the
  side.
- 'scrollInvisible': shortcut for setting invisible style. Useful with scroll
  overlay, since it allows scrolling without taking up space or hiding content.
- 'scrollFollowFocus': whether to auto scroll when focusing a non visible item.
- 'scrollStyle': the base style of the scroll bar.
-}
data ScrollCfg s e = ScrollCfg {
  _scScrollType :: Maybe ScrollType,
  _scScrollOverlay :: Maybe Bool,
  _scScrollFwdStyle :: Maybe (WidgetEnv s e -> Style -> (Style, Style)),
  _scFollowFocus :: Maybe Bool,
  _scWheelRate :: Maybe Rational,
  _scBarColor :: Maybe Color,
  _scBarHoverColor :: Maybe Color,
  _scThumbColor :: Maybe Color,
  _scThumbHoverColor :: Maybe Color,
  _scStyle :: Maybe (ALens' ThemeState StyleState),
  _scBarWidth :: Maybe Double,
  _scThumbWidth :: Maybe Double,
  _scThumbRadius :: Maybe Double
}

instance Default (ScrollCfg s e) where
  def = ScrollCfg {
    _scScrollType = Nothing,
    _scScrollOverlay = Nothing,
    _scScrollFwdStyle = Nothing,
    _scFollowFocus = Nothing,
    _scWheelRate = Nothing,
    _scBarColor = Nothing,
    _scBarHoverColor = Nothing,
    _scThumbColor = Nothing,
    _scThumbHoverColor = Nothing,
    _scStyle = Nothing,
    _scBarWidth = Nothing,
    _scThumbWidth = Nothing,
    _scThumbRadius = Nothing
  }

instance Semigroup (ScrollCfg s e) where
  (<>) t1 t2 = ScrollCfg {
    _scScrollType = _scScrollType t2 <|> _scScrollType t1,
    _scScrollOverlay = _scScrollOverlay t2 <|> _scScrollOverlay t1,
    _scScrollFwdStyle = _scScrollFwdStyle t2 <|> _scScrollFwdStyle t1,
    _scFollowFocus = _scFollowFocus t2 <|> _scFollowFocus t1,
    _scWheelRate = _scWheelRate t2 <|> _scWheelRate t1,
    _scBarColor = _scBarColor t2 <|> _scBarColor t1,
    _scBarHoverColor = _scBarHoverColor t2 <|> _scBarHoverColor t1,
    _scThumbColor = _scThumbColor t2 <|> _scThumbColor t1,
    _scThumbHoverColor = _scThumbHoverColor t2 <|> _scThumbHoverColor t1,
    _scStyle = _scStyle t2 <|> _scStyle t1,
    _scBarWidth = _scBarWidth t2 <|> _scBarWidth t1,
    _scThumbWidth = _scThumbWidth t2 <|> _scThumbWidth t1,
    _scThumbRadius = _scThumbRadius t2 <|> _scThumbRadius t1
  }

instance Monoid (ScrollCfg s e) where
  mempty = def

instance CmbWheelRate (ScrollCfg s e) Rational where
  wheelRate rate = def {
    _scWheelRate = Just rate
  }

instance CmbBarColor (ScrollCfg s e) where
  barColor col = def {
    _scBarColor = Just col
  }

instance CmbBarHoverColor (ScrollCfg s e) where
  barHoverColor col = def {
    _scBarHoverColor = Just col
  }

instance CmbBarWidth (ScrollCfg s e) where
  barWidth w = def {
    _scBarWidth = Just w
  }

-- Thumb
instance CmbThumbColor (ScrollCfg s e) where
  thumbColor col = def {
    _scThumbColor = Just col
  }

instance CmbThumbHoverColor (ScrollCfg s e) where
  thumbHoverColor col = def {
    _scThumbHoverColor = Just col
  }

instance CmbThumbWidth (ScrollCfg s e) where
  thumbWidth w = def {
    _scThumbWidth = Just w
  }

instance CmbThumbRadius (ScrollCfg s e) where
  thumbRadius r = def {
    _scThumbRadius = Just r
  }

-- | Scroll bars will be displayed on top of the content.
scrollOverlay :: ScrollCfg s e
scrollOverlay = scrollOverlay_ True

{-|
Sets whether scroll bars will be displayed on top of the content or next to it.
-}
scrollOverlay_ :: Bool -> ScrollCfg s e
scrollOverlay_ overlay = def {
  _scScrollOverlay = Just overlay
}

{-|
Sets a function that will split the node's style into one for the scroll and one
for the child node. Useful for widgets which wrap themselves in a scroll, such
as textArea, to be able to receive customizations made by the user.
-}
scrollFwdStyle :: (WidgetEnv s e -> Style -> (Style, Style)) -> ScrollCfg s e
scrollFwdStyle fwd = def {
  _scScrollFwdStyle = Just fwd
}

-- | Default style forward function, keeping standard fields for scroll.
scrollFwdDefault :: WidgetEnv s e -> Style -> (Style, Style)
scrollFwdDefault wenv style = (scrollStyle, childStyle) where
  scrollStyle = def
    & collectStyleField_ L.sizeReqW style
    & collectStyleField_ L.sizeReqH style
    & collectStyleField_ L.border style
    & collectStyleField_ L.radius style
    & collectStyleField_ L.bgColor style
  childStyle = def
    & collectStyleField_ L.padding style
    & collectStyleField_ L.fgColor style
    & collectStyleField_ L.sndColor style
    & collectStyleField_ L.hlColor style
    & collectStyleField_ L.text style
    & collectStyleField_ L.cursorIcon style

-- | Sets the style of the scroll bars to transparent.
scrollInvisible :: ScrollCfg s e
scrollInvisible = scrollInvisible_ True

-- | Whether to set the style of the scroll bars to transparent.
scrollInvisible_ :: Bool -> ScrollCfg s e
scrollInvisible_ False = def
scrollInvisible_ True = def {
  _scScrollOverlay = Just True,
  _scBarColor = Just transparent,
  _scBarHoverColor = Just transparent,
  _scThumbColor = Just transparent,
  _scThumbHoverColor = Just transparent
}

-- | Makes the scroll automatically follow focused items to make them visible.
scrollFollowFocus :: ScrollCfg s e
scrollFollowFocus = scrollFollowFocus_ True

-- | Whether to automatically follow focused items to make them visible.
scrollFollowFocus_ :: Bool -> ScrollCfg s e
scrollFollowFocus_ follow = def {
  _scFollowFocus = Just follow
}

{-|
Sets the base style of the scroll bar. Useful when creating widgets which use
scroll and may need to customize it.
-}
scrollStyle :: ALens' ThemeState StyleState -> ScrollCfg s e
scrollStyle style = def {
  _scStyle = Just style
}

-- Not exported
scrollType :: ScrollType -> ScrollCfg s e
scrollType st = def {
  _scScrollType = Just st
}

data ScrollState = ScrollState {
  _sstDragging :: Maybe ActiveBar,
  _sstDeltaX :: !Double,
  _sstDeltaY :: !Double,
  _sstVpSize :: Size,
  _sstChildSize :: Size,
  _sstScissor :: Rect
} deriving (Eq, Show, Generic)

data ScrollContext = ScrollContext {
  hScrollRatio :: Double,
  vScrollRatio :: Double,
  hScrollRequired :: Bool,
  vScrollRequired :: Bool,
  hMouseInScroll :: Bool,
  vMouseInScroll :: Bool,
  hMouseInThumb :: Bool,
  vMouseInThumb :: Bool,
  hScrollRect :: Rect,
  vScrollRect :: Rect,
  hThumbRect :: Rect,
  vThumbRect :: Rect
} deriving (Eq, Show)

instance Default ScrollState where
  def = ScrollState {
    _sstDragging = Nothing,
    _sstDeltaX = 0,
    _sstDeltaY = 0,
    _sstVpSize = def,
    _sstChildSize = def,
    _sstScissor = def
  }

-- | Messages the scroll component supports.
data ScrollMessage
  -- | Causes the scroll to update its bars to ensure rect is visible.
  = ScrollTo Rect
  -- | Sets both bars to zero.
  | ScrollReset
  deriving (Eq, Show)

-- | Creates a scroll node that may show both bars.
scroll :: WidgetNode s e -> WidgetNode s e
scroll managedWidget = scroll_ def managedWidget

-- | Creates a scroll node that may show both bars. Accepts config.
scroll_ :: [ScrollCfg s e] -> WidgetNode s e -> WidgetNode s e
scroll_ configs managed = makeNode (makeScroll config def) managed where
  config = mconcat configs

-- | Creates a horizontal scroll node. Vertical space is equal to what the
--   parent node assigns.
hscroll :: WidgetNode s e -> WidgetNode s e
hscroll managedWidget = hscroll_ def managedWidget

-- | Creates a horizontal scroll node. Vertical space is equal to what the
--   parent node assigns. Accepts config.
hscroll_ :: [ScrollCfg s e] -> WidgetNode s e -> WidgetNode s e
hscroll_ configs managed = makeNode (makeScroll config def) managed where
  config = mconcat (scrollType ScrollH : configs)

-- | Creates a vertical scroll node. Vertical space is equal to what the
--   parent node assigns.
vscroll :: WidgetNode s e -> WidgetNode s e
vscroll managedWidget = vscroll_ def managedWidget

-- | Creates a vertical scroll node. Vertical space is equal to what the
--   parent node assigns. Accepts config.
vscroll_ :: [ScrollCfg s e] -> WidgetNode s e -> WidgetNode s e
vscroll_ configs managed = makeNode (makeScroll config def) managed where
  config = mconcat (scrollType ScrollV : configs)

makeNode :: Widget s e -> WidgetNode s e -> WidgetNode s e
makeNode widget managedWidget = defaultWidgetNode "scroll" widget
  & L.info . L.focusable .~ False
  & L.children .~ Seq.singleton managedWidget

makeScroll :: ScrollCfg s e -> ScrollState -> Widget s e
makeScroll config state = widget where
  container = def {
    containerChildrenOffset = Just offset,
    containerChildrenScissor = Just (_sstScissor state),
    containerLayoutDirection = layoutDirection,
    containerGetBaseStyle = getBaseStyle,
    containerGetCurrentStyle = scrollCurrentStyle,
    containerCreateContainerFromModel = createContainerFromModel,
    containerUpdateCWenv = updateCWenv,
    containerInit = init,
    containerMerge = merge,
    containerFindByPoint = findByPoint,
    containerHandleEvent = handleEvent,
    containerHandleMessage = handleMessage,
    containerGetSizeReq = getSizeReq,
    containerResize = resize,
    containerRenderAfter = renderAfter
  }
  widget = createContainer state container

  ScrollState dragging dx dy _ _ _ = state
  Size childWidth childHeight = _sstChildSize state
  Size maxVpW maxVpH = _sstVpSize state
  offset = Point dx dy
  scrollType = fromMaybe ScrollBoth (_scScrollType config)
  layoutDirection = case scrollType of
    ScrollH -> LayoutHorizontal
    ScrollV -> LayoutVertical
    ScrollBoth -> LayoutNone

  getBaseStyle wenv node = _scStyle config >>= handler where
    handler lstyle = Just $ collectTheme wenv (cloneLens lstyle)

  checkFwdStyle wenv node = newNode where
    fwdStyle = _scScrollFwdStyle config
    style = node ^. L.info . L.style
    (parentStyle, childStyle)
      | isJust fwdStyle = fromJust fwdStyle wenv style
      | otherwise = def
    newNode
      | isJust fwdStyle = node
        & L.info . L.style .~ parentStyle
        & L.children . ix 0 . L.info . L.style .~ childStyle
      | otherwise = node

  createContainerFromModel wenv node state = Just newContainer where
    offset = Point (_sstDeltaX state) (_sstDeltaX state)
    newContainer = container {
      containerChildrenOffset = Just offset
    }

  -- This is overriden to account for space used by scroll bars
  updateCWenv wenv node cnode cidx = newWenv where
    theme = currentTheme wenv node
    barW = fromMaybe (theme ^. L.scrollBarWidth) (_scBarWidth config)
    overlay = fromMaybe (theme ^. L.scrollOverlay) (_scScrollOverlay config)

    ScrollContext{..} = scrollStatus config wenv node state (Point 0 0)
    style = currentStyle wenv node
    carea = getContentArea node style

    -- barH consumes vertical space, barV consumes horizontal space
    barH
      | hScrollRequired && not overlay = barW
      | otherwise = 0
    barV
      | vScrollRequired && not overlay = barW
      | otherwise = 0
    clientArea = subtractFromRect carea 0 barV 0 barH

    newWenv = wenv
      & L.viewport .~ moveRect (negPoint offset) (fromMaybe carea clientArea)

  init wenv node = resultNode newNode where
    newNode = checkFwdStyle wenv node

  merge wenv node oldNode oldState = resultNode newNode where
    newNode = checkFwdStyle wenv $ node
      & L.widget .~ makeScroll config oldState

  findByPoint wenv node start point = result where
    -- The point argument already has offset applied
    scrollPoint = subPoint point offset
    sctx = scrollStatus config wenv node state scrollPoint
    mouseInScroll
      =  (hMouseInScroll sctx && hScrollRequired sctx)
      || (vMouseInScroll sctx && vScrollRequired sctx)

    child = Seq.index (node ^. L.children) 0
    childHovered = isPointInNodeVp child point
    childDragged = isNodePressed wenv child
    result
      | (not mouseInScroll && childHovered) || childDragged = Just 0
      | otherwise = Nothing

  handleEvent wenv node target evt = case evt of
    Focus{} -> result where
      overlay = wenv ^. L.overlayPath
      inOverlay info
        | isJust overlay = seqStartsWith (fromJust overlay) (info ^. L.path)
        | otherwise = False
      focusPath = wenv ^. L.focusedPath
      focusInst = findInstOrScroll wenv node focusPath
      focusVp = focusInst ^? _Just . L.viewport
      focusOverlay = maybe False inOverlay focusInst

      follow = fromMaybe (theme ^. L.scrollFollowFocus) (_scFollowFocus config)
      overlayMatch = focusOverlay == inOverlay (node ^. L.info)

      result
        | follow && overlayMatch = focusVp >>= scrollTo wenv node
        | otherwise = Nothing

    ButtonAction point btn status _ -> result where
      mainPressed = status == BtnPressed && btn == wenv ^. L.mainButton
      mainReleased = status == BtnReleased && btn == wenv ^. L.mainButton

      isDragging = isJust $ _sstDragging state

      startDragH = mainPressed && not isDragging && hMouseInThumb sctx
      startDragV = mainPressed && not isDragging && vMouseInThumb sctx

      jumpScrollH = mainPressed && not isDragging && hMouseInScroll sctx
      jumpScrollV = mainPressed && not isDragging && vMouseInScroll sctx

      mouseInThumb = hMouseInThumb sctx || vMouseInThumb sctx
      mouseInScroll = hMouseInScroll sctx || vMouseInScroll sctx

      newState
        | startDragH = state { _sstDragging = Just HBar }
        | startDragV = state { _sstDragging = Just VBar }
        | jumpScrollH = updateScrollThumb state HBar point contentArea sctx
        | jumpScrollV = updateScrollThumb state VBar point contentArea sctx
        | mainReleased = state { _sstDragging = Nothing }
        | otherwise = state

      newRes = rebuildWidget wenv node newState
      handledResult = Just $ newRes
        & L.requests <>~ Seq.fromList scrollReqs
      result
        | mainPressed && (mouseInThumb || mouseInScroll) = handledResult
        | mainReleased && isDragging = handledResult
        | otherwise = Nothing

    Move point | isJust dragging -> result where
      drag bar = updateScrollThumb state bar point contentArea sctx
      makeWidget state = rebuildWidget wenv node state
      makeResult state = makeWidget state
        & L.requests <>~ Seq.fromList (RenderOnce : scrollReqs)
      result = fmap (makeResult . drag) dragging

    Move point | isNothing dragging -> result where
      mousePosPrev = wenv ^. L.inputStatus . L.mousePosPrev
      psctx = scrollStatus config wenv node state mousePosPrev
      changed
        = hMouseInThumb sctx /= hMouseInThumb psctx
        || vMouseInThumb sctx /= vMouseInThumb psctx
        || hMouseInScroll sctx /= hMouseInScroll psctx
        || vMouseInScroll sctx /= vMouseInScroll psctx
      result
        | changed = Just $ resultReqs node [RenderOnce]
        | otherwise = Nothing

    WheelScroll _ (Point wx wy) wheelDirection -> result where
      changedX = wx /= 0 && childWidth > cw
      changedY = wy /= 0 && childHeight > ch

      needsUpdate = changedX || changedY
      makeWidget state = rebuildWidget wenv node state
      makeResult state = makeWidget state
        & L.requests <>~ Seq.fromList scrollReqs

      result
        | needsUpdate = Just $ makeResult newState
        | otherwise = Nothing
      stepX = wheelRate * wx
      stepY = wheelRate * wy
      newState = state {
        _sstDeltaX = scrollAxisH (stepX + dx),
        _sstDeltaY = scrollAxisV (stepY + dy)
      }

    _ -> Nothing
    where
      theme = currentTheme wenv node
      style = scrollCurrentStyle wenv node
      contentArea = getContentArea node style
      mousePos = wenv ^. L.inputStatus . L.mousePos

      Rect cx cy cw ch = contentArea
      sctx = scrollStatus config wenv node state mousePos
      scrollReqs = [IgnoreParentEvents]
      wheelCfg = fromMaybe (theme ^. L.scrollWheelRate) (_scWheelRate config)
      wheelRate = fromRational wheelCfg

  scrollAxis reqDelta childLength vpLength
    | maxDelta == 0 = 0
    | reqDelta < 0 = max reqDelta (-maxDelta)
    | otherwise = min reqDelta 0
    where
      maxDelta = max 0 (childLength - vpLength)
  scrollAxisH delta = scrollAxis delta childWidth maxVpW
  scrollAxisV delta = scrollAxis delta childHeight maxVpH

  handleMessage wenv node target message = result where
    handleScrollMessage (ScrollTo rect) = scrollTo wenv node rect
    handleScrollMessage ScrollReset = scrollReset wenv node
    result = cast message >>= handleScrollMessage

  scrollTo wenv node targetRect = result where
    style = scrollCurrentStyle wenv node
    contentArea = getContentArea node style

    rect = moveRect offset targetRect
    Rect rx ry rw rh = rect
    Rect cx cy _ _ = contentArea

    diffL = cx - rx
    diffR = cx + maxVpW - (rx + rw)
    diffT = cy - ry
    diffB = cy + maxVpH - (ry + rh)

    stepX
      | rectInRectH rect contentArea = dx
      | abs diffL <= abs diffR = diffL + dx
      | otherwise = diffR + dx
    stepY
      | rectInRectV rect contentArea = dy
      | abs diffT <= abs diffB = diffT + dy
      | otherwise = diffB + dy

    newState = state {
      _sstDeltaX = scrollAxisH stepX,
      _sstDeltaY = scrollAxisV stepY
    }
    result
      | rectInRect rect contentArea = Nothing
      | otherwise = Just $ rebuildWidget wenv node newState

  scrollReset wenv node = result where
    newState = state {
      _sstDeltaX = 0,
      _sstDeltaY = 0
    }
    result = Just $ rebuildWidget wenv node newState

  updateScrollThumb state activeBar point contentArea sctx = newState where
    Point px py = point
    ScrollContext{..} = sctx
    Rect cx cy _ _ = contentArea

    hMid = _rW hThumbRect / 2
    vMid = _rH vThumbRect / 2

    hDelta = (cx - px + hMid) / hScrollRatio
    vDelta = (cy - py + vMid) / vScrollRatio

    newDeltaX
      | activeBar == HBar = scrollAxisH hDelta
      | otherwise = dx
    newDeltaY
      | activeBar == VBar = scrollAxisV vDelta
      | otherwise = dy

    newState = state {
      _sstDeltaX = newDeltaX,
      _sstDeltaY = newDeltaY
    }

  rebuildWidget wenv node newState = result where
    newNode = node
      & L.widget .~ makeScroll config newState
    result = resultNode newNode

  getSizeReq :: ContainerGetSizeReqHandler s e
  getSizeReq wenv node children = sizeReq where
    style = scrollCurrentStyle wenv node
    child = Seq.index children 0

    tw = sizeReqMaxBounded $ child ^. L.info . L.sizeReqW
    th = sizeReqMaxBounded $ child ^. L.info . L.sizeReqH

    Size w h = fromMaybe def (addOuterSize style (Size tw th))
    factor = 1

    sizeReq = (expandSize w factor, expandSize h factor)

  resize wenv node viewport children = result where
    theme = currentTheme wenv node
    style = scrollCurrentStyle wenv node

    Rect cl ct cw ch = fromMaybe def (removeOuterBounds style viewport)
    dx = _sstDeltaX state
    dy = _sstDeltaY state

    child = Seq.index (node ^. L.children) 0
    childW = sizeReqMaxBounded $ child ^. L.info . L.sizeReqW
    childH = sizeReqMaxBounded $ child ^. L.info . L.sizeReqH

    barW = fromMaybe (theme ^. L.scrollBarWidth) (_scBarWidth config)
    overlay = fromMaybe (theme ^. L.scrollOverlay) (_scScrollOverlay config)

    (ncw, nch)
      | not overlay = (cw - barW, ch - barW)
      | otherwise = (cw, ch)
    (maxW, areaW)
      | scrollType == ScrollV && childH > ch = (ncw, ncw)
      | scrollType == ScrollV = (cw, cw)
      | scrollType == ScrollH = (cw, max cw childW)
      | childH <= ch && childW <= cw = (cw, cw)
      | childH <= ch = (cw, max cw childW)
      | otherwise = (ncw, max ncw childW)
    (maxH, areaH)
      | scrollType == ScrollH && childW > cw = (nch, nch)
      | scrollType == ScrollH = (ch, ch)
      | scrollType == ScrollV = (ch, max ch childH)
      | childW <= cw && childH <= ch = (ch, ch)
      | childW <= cw = (ch, max ch childH)
      | otherwise = (nch, max nch childH)

    newDx = scrollAxis dx areaW maxW
    newDy = scrollAxis dy areaH maxH

    scissor = Rect cl ct maxW maxH
    cViewport = Rect cl ct areaW areaH

    newState = state {
      _sstDeltaX = newDx,
      _sstDeltaY = newDy,
      _sstVpSize = Size maxW maxH,
      _sstChildSize = Size areaW areaH,
      _sstScissor = scissor
    }
    newNode = resultNode $ node
      & L.widget .~ makeScroll config newState
    result = (newNode, Seq.singleton cViewport)

  renderAfter wenv node renderer = do
    when hScrollRequired $
      drawRect renderer hScrollRect barColorH Nothing

    when vScrollRequired $
      drawRect renderer vScrollRect barColorV Nothing

    when hScrollRequired $
      drawRect renderer hThumbRect thumbColorH thumbRadius

    when vScrollRequired $
      drawRect renderer vThumbRect thumbColorV thumbRadius
    where
      ScrollContext{..} = scrollStatus config wenv node state mousePos
      mousePos = wenv ^. L.inputStatus . L.mousePos

      draggingH = _sstDragging state == Just HBar
      draggingV = _sstDragging state == Just VBar

      theme = wenv ^. L.theme
      athm = currentTheme wenv node
      tmpRad = fromMaybe (athm ^. L.scrollThumbRadius) (_scThumbRadius config)
      thumbRadius
        | tmpRad > 0 = Just (radius tmpRad)
        | otherwise = Nothing

      cfgBarBCol = _scBarColor config
      cfgBarHCol = _scBarHoverColor config
      cfgThumbBCol = _scThumbColor config
      cfgThumbHCol = _scThumbHoverColor config

      barBCol = cfgBarBCol <|> Just (theme ^. L.basic . L.scrollBarColor)
      barHCol = cfgBarHCol <|> Just (theme ^. L.hover . L.scrollBarColor)

      thumbBCol = cfgThumbBCol <|> Just (theme ^. L.basic . L.scrollThumbColor)
      thumbHCol = cfgThumbHCol <|> Just (theme ^. L.hover. L.scrollThumbColor)

      barColorH
        | hMouseInScroll = barHCol
        | otherwise = barBCol
      barColorV
        | vMouseInScroll = barHCol
        | otherwise = barBCol
      thumbColorH
        | hMouseInThumb || draggingH = thumbHCol
        | otherwise = thumbBCol
      thumbColorV
        | vMouseInThumb || draggingV = thumbHCol
        | otherwise = thumbBCol

scrollCurrentStyle :: WidgetEnv s e -> WidgetNode s e -> StyleState
scrollCurrentStyle wenv node
  | isNodeFocused wenv child = focusedStyle wenv node
  | otherwise = currentStyle wenv node
  where
    child = node ^. L.children ^?! ix 0

scrollStatus
  :: ScrollCfg s e
  -> WidgetEnv s e
  -> WidgetNode s e
  -> ScrollState
  -> Point
  -> ScrollContext
scrollStatus config wenv node scrollState mousePos = ScrollContext{..} where
  ScrollState _ dx dy _ _ _ = scrollState
  Size childWidth childHeight = _sstChildSize scrollState
  Size vpWidth vpHeight = _sstVpSize scrollState
  theme = currentTheme wenv node
  style = scrollCurrentStyle wenv node
  contentArea = getContentArea node style

  barW = fromMaybe (theme ^. L.scrollBarWidth) (_scBarWidth config)
  thumbW = fromMaybe (theme ^. L.scrollThumbWidth) (_scThumbWidth config)

  caLeft = _rX contentArea
  caTop = _rY contentArea
  caWidth = _rW contentArea
  caHeight = _rH contentArea

  hScrollTop = caHeight - barW
  vScrollLeft = caWidth - barW

  hRatio = caWidth / childWidth
  vRatio = caHeight / childHeight
  hRatioR = (caWidth - barW) / childWidth
  vRatioR = (caHeight - barW) / childHeight

  (hScrollRatio, vScrollRatio)
    | hRatio < 1 && vRatio < 1 = (hRatioR, vRatioR)
    | otherwise = (hRatio, vRatio)
  hScrollRequired = hScrollRatio < 1
  vScrollRequired = vScrollRatio < 1

  hScrollRect = Rect {
    _rX = caLeft,
    _rY = caTop + hScrollTop,
    _rW = vpWidth,
    _rH = barW
  }
  vScrollRect = Rect {
    _rX = caLeft + vScrollLeft,
    _rY = caTop,
    _rW = barW,
    _rH = vpHeight
  }
  hThumbRect = Rect {
    _rX = caLeft - hScrollRatio * dx,
    _rY = caTop + hScrollTop + (barW - thumbW) / 2,
    _rW = hScrollRatio * vpWidth,
    _rH = thumbW
  }
  vThumbRect = Rect {
    _rX = caLeft + vScrollLeft + (barW - thumbW) / 2,
    _rY = caTop - vScrollRatio * dy,
    _rW = thumbW,
    _rH = vScrollRatio * vpHeight
  }

  hMouseInScroll = pointInRect mousePos hScrollRect
  vMouseInScroll = pointInRect mousePos vScrollRect

  hMouseInThumb = pointInRect mousePos hThumbRect
  vMouseInThumb = pointInRect mousePos vThumbRect

findInstOrScroll
  :: WidgetEnv s e -> WidgetNode s e -> Seq.Seq PathStep -> Maybe WidgetNodeInfo
findInstOrScroll wenv node target = wniScroll <|> wniTarget where
  child = Seq.index (node ^. L.children) 0
  isScroll wni = wni ^. L.widgetType == "scroll"
  branch = widgetFindBranchByPath (child ^. L.widget) wenv child target
  scrolls = Seq.filter isScroll branch
  wniTarget = Seq.lookup (length branch - 1) branch
  wniScroll = Seq.lookup (length scrolls - 1) scrolls