packages feed

UISF-0.3.0.0: FRP/UISF/UITypes.hs

-----------------------------------------------------------------------------
-- |
-- Module      :  FRP.UISF.UIMonad
-- Copyright   :  (c) Daniel Winograd-Cort 2014
-- License     :  see the LICENSE file in the distribution
--
-- Maintainer  :  dwc@cs.yale.edu
-- Stability   :  experimental

{-# LANGUAGE RecursiveDo #-}

module FRP.UISF.UITypes where

import FRP.UISF.SOE
import FRP.UISF.AuxFunctions (mergeE)

------------------------------------------------------------
-- * UI Types
------------------------------------------------------------

-- $uitypes
-- Widgets are arrows that map multiple inputs to multiple outputs.  
-- Additionally, they have a relatively static layout argument that, 
-- while it can change over time, is not dependent on any of its 
-- inputs at any given moment.
--
-- On the input end, a widget will accept:
--
--  - a graphical context, 
--
--  - some information about which widget is in focus (for the purposes 
--    of routing key presses and mouse clicks and potentially for drawing 
--    the widget differently), 
--
--  - and the current time.
--
-- On the output end, a widget will produce from these inputs:
-- 
--  - an indicator of whether the widget needs to be redrawn,
-- 
--  - any focus information that needs to be conveyed to future widgets, 
-- 
--  - the graphics to render to display this widget,
-- 
--  - and any new ThreadIds to keep track of (for proper shutdown when finished).
--
-- Additionally, as widgets are generic arrows, there will be a parameterized 
-- inputs and output type.
--
-- In this file, we will declare the various types to make creating the overall 
-- UI possible.  For the widget type itself, see UISF in FRP.UISF.UISF.


------------------------------------------------------------
-- * Control Data
------------------------------------------------------------

-- | The control data is simply a list of Thread Ids.
type TerminationProc = Maybe (IO ())

-- | No new thread ids.
nullTP :: TerminationProc
nullTP = Nothing

-- | A method for merging to control data objects.
mergeTP :: TerminationProc -> TerminationProc -> TerminationProc
mergeTP = mergeE (>>)


------------------------------------------------------------
-- * Rendering Context
------------------------------------------------------------

-- | A rendering context specifies the following:

data CTX = CTX 
  { flow   :: Flow 
        -- ^ A layout direction to flow widgets. 
  
  , bounds :: Rect 
        -- ^ A rectangle bound of current drawing area to render a UI
        --   component. It specifies the max size of a widget, not the
        --   actual size. It's up to each individual widget to decide
        --   where in this bound to put itself.
  
  , isConjoined :: Bool 
        -- ^ A flag to tell whether we are in a conjoined state or not.  
        -- A conjoined context will duplicate itself for subcomponents 
        -- rather than splitting.  This can be useful for making compound 
        -- widgets when one widget takes up space and the other performs 
        -- some side effect having to do with that space.
  } deriving Show

-- | Flow determines widget ordering.
data Flow = TopDown | BottomUp | LeftRight | RightLeft deriving (Eq, Show)
-- | A dimension specifies size.
type Dimension = (Int, Int)
-- | A rectangle has a corner point and a dimension.
type Rect = (Point, Dimension)


------------------------------------------------------------
-- * UI Layout
------------------------------------------------------------

-- $ctc The layout of a widget provides data to calculate its actual size
-- in a given context.  
-- Layout calculation makes use of lazy evaluation to do everything in one pass.  
-- Although the UI function maps from Context to Layout, all of the fields of 
-- Layout must be independent of the Context so that they are avaiable before 
-- the UI function is even evaluated.

-- | Layouts for individual widgets typically come in a few standard flavors, 
--   so we have this convenience function for their creation.
--   This function takes layout information for first the horizontal 
--   dimension and then the vertical.
makeLayout :: LayoutType ->     -- ^ Horizontal Layout information
              LayoutType ->     -- ^ Vertical Layout information
              Layout
makeLayout (Fixed h) (Fixed v) = Layout 0 0 h v 0 0
makeLayout (Stretchy minW) (Fixed v) = Layout 1 0 0 v minW 0
makeLayout (Fixed h) (Stretchy minH) = Layout 0 1 h 0 0 minH
makeLayout (Stretchy minW) (Stretchy minH) = Layout 1 1 0 0 minW minH

-- | A dimension can either be:
data LayoutType = 
        Stretchy { minSize :: Int }
        -- ^ Stretchy with a minimum size in pixels
      | Fixed { fixedSize :: Int }
        -- ^ Fixed with a size measured in pixels

-- | The null layout is useful for \"widgets\" that do not appear or 
--   take up space on the screen.
nullLayout = Layout 0 0 0 0 0 0


-- | More complicated layouts can be manually constructed with direct 
-- access to the Layout data type.
--
-- 1. hFill and vFill specify how much stretching space (in comparative 
--    units) in the horizontal and vertical directions should be 
--    allocated for this widget.
-- 
-- 2. hFixed and vFixed specify how much non-stretching space (in pixels) 
--    of width and height should be allocated for this widget.
-- 
-- 3. minW and minH specify minimum values (in pixels) of width and height 
--    for the widget's stretchy dimensions.

data Layout = Layout
  { hFill  :: Int
  , vFill  :: Int
  , hFixed :: Int
  , vFixed :: Int
  , minW   :: Int
  , minH   :: Int
  } deriving (Eq, Show)



------------------------------------------------------------
-- * Context and Layout Functions
------------------------------------------------------------

---------------
-- divideCTX --
---------------
-- | Divides the CTX among the two given layouts.

divideCTX :: CTX -> Layout -> Layout -> (CTX, CTX)
divideCTX ctx@(CTX a ((x, y), (w, h)) c) 
          ~(Layout wFill  hFill  wFixed  hFixed  wMin  hMin) 
          ~(Layout wFill' hFill' wFixed' hFixed' wMin' hMin') =
  if c then (ctx, ctx) else
  case a of
    TopDown   -> (CTX a ((x, y),                 (w1T, h1T)) c, 
                  CTX a ((x, y + h1T),           (w2T, h2T)) c)
    BottomUp  -> (CTX a ((x, y + h - h1T),       (w1T, h1T)) c, 
                  CTX a ((x, y + h - h1T - h2T), (w2T, h2T)) c)
    LeftRight -> (CTX a ((x, y),                 (w1L, h1L)) c, 
                  CTX a ((x + w1L, y),           (w2L, h2L)) c)
    RightLeft -> (CTX a ((x + w - w1L, y),       (w1L, h1L)) c, 
                  CTX a ((x + w - w1L - w2L, y), (w2L, h2L)) c)
  where
    -- The commented out code here forces the contexts to match exactly 
    -- what the layout requests.  The code in place matches to the first 
    -- layout and then gives the rest of the context to the second.
    -- A more robust design may require a special "filler" layout that 
    -- is not stretchy but will accept any leftover pixels.  We could 
    -- then have a filler widget that is essentially (arr id) with this 
    -- special layout.
    wportion fill = div' (fill * (w - wFixed - wFixed')) (wFill + wFill')
    (w1L,w2L) = let w1 = wFixed  + max wMin  (wportion wFill)
                    w2 = wFixed' + max wMin' (wportion wFill')
                in (w1, w-w1) --if w1+w2 > w then (w1, w-w1) else (w1, w2)
    h1L = h --max hMin  (if hFill  == 0 then hFixed  else h)
    h2L = h --max hMin' (if hFill' == 0 then hFixed' else h)
    hportion fill = div' (fill * (h - hFixed - hFixed')) (hFill + hFill')
    (h1T,h2T) = let h1 = hFixed  + max hMin  (hportion hFill)
                    h2 = hFixed' + max hMin' (hportion hFill')
                in (h1, h-h1) --if h1+h2 > h then (h1, h-h1) else (h1, h2)
    w1T = w --max wMin  (if wFill  == 0 then wFixed  else w)
    w2T = w --max wMin' (if wFill' == 0 then wFixed' else w)
    div' b 0 = 0
    div' b d = div b d


-----------------
-- mergeLayout --
-----------------
-- | Merge two layouts into one.

mergeLayout :: Flow -> Layout -> Layout -> Layout
mergeLayout a (Layout n m u v minw minh) (Layout n' m' u' v' minw' minh') = 
  case a of
    TopDown   -> Layout (max' n n') (m + m') (max u u') (v + v') (max minw minw') (minh + minh')
    BottomUp  -> Layout (max' n n') (m + m') (max u u') (v + v') (max minw minw') (minh + minh')
    LeftRight -> Layout (n + n') (max' m m') (u + u') (max v v') (minw + minw') (max minh minh')
    RightLeft -> Layout (n + n') (max' m m') (u + u') (max v v') (minw + minw') (max minh minh')
  where
    max' 0 0 = 0
    max' _ _ = 1


------------------------------------------------------------
-- * Graphics and System State
------------------------------------------------------------

-- | Merging two graphics can be achieved with overGraphic, but 
-- the mergeGraphic function additionally constrains the graphics 
-- based on their layouts and the context.
-- TODO: Make sure this works as well as it should
mergeGraphics :: CTX -> (Graphic, Layout) -> (Graphic, Layout) -> Graphic
mergeGraphics ctx (g1, l1) (g2, l2) = case (l1 == nullLayout, l2 == nullLayout) of
  (True,  True)  -> nullGraphic
  (True,  False) -> g2
  (False, True)  -> g1
  (False, False) -> overGraphic g2 g1


-- The Focus and DirtyBit types are for system state.

-- | The Focus type helps focusable widgets communicate with each 
-- other about which widget is in focus.  It consists of a WidgetID 
-- and a FocusInfo.
type Focus = (WidgetID, FocusInfo)

-- | The WidgetID for any given widget is dynamic based 
-- on how many focusable widgets are active at the moment.  It is designed 
-- basically as a counter that focusable widgets will automatically (via the 
-- focusable function) increment.
type WidgetID = Int

-- | The FocusInfo means one of the following:
data FocusInfo = 
        HasFocus
        -- ^ Indicates that this widget is a subwidget of 
        --   a widget that is in focus.  Thus, this widget too is in focus, and 
        --   this widget should pass HasFocus forward.
      | NoFocus 
        -- ^ Indicates that there is no focus information to 
        --   communicate between widgets.
      | SetFocusTo WidgetID
        -- ^ Indicates that the widget whose id is given 
        --   should take focus.  That widget should then pass NoFocus onward.
  deriving (Show, Eq)

-- | The dirty bit is a bit to indicate if the widget needs to be redrawn.
type DirtyBit = Bool