packages feed

tinytools-0.1.0.6: src/Potato/Flow/Controller/Manipulator/Box.hs

-- This handler does the following things 
-- - transform any selection (drag + resize)
-- - create boxes (consider splitting this one out)
-- - go to box text label or text area edit handler

{-# LANGUAGE RecordWildCards #-}
{-# OPTIONS_GHC -fno-warn-unused-record-wildcards #-}


module Potato.Flow.Controller.Manipulator.Box where

import           Relude

import           Potato.Flow.Controller.Handler
import           Potato.Flow.Controller.Input
import           Potato.Flow.Controller.Manipulator.BoxText
import           Potato.Flow.Controller.Manipulator.TextArea
import           Potato.Flow.Controller.Manipulator.Common
import           Potato.Flow.Controller.Types
import           Potato.Flow.Math
import           Potato.Flow.Methods.SEltMethods
import           Potato.Flow.Serialization.Snake
import           Potato.Flow.Types
import           Potato.Flow.OwlItem
import Potato.Flow.Owl
import Potato.Flow.OwlState
import Potato.Flow.Methods.Types
import Potato.Flow.Llama
import           Potato.Flow.Methods.LlamaWorks
import           Potato.Flow.Preview


import           Data.Default
import           Data.Dependent.Sum                         (DSum ((:=>)))
import qualified Data.IntMap                                as IM
import qualified Data.Map as Map
import qualified Data.List                                  as L
import qualified Data.Sequence as Seq

import Control.Exception (assert)



superOwl_isTransformable :: (HasOwlTree o) => SuperOwl -> o -> Bool
superOwl_isTransformable sowl ot = case _owlItem_subItem (_superOwl_elt sowl) of
  OwlSubItemNone -> False
  OwlSubItemFolder _ -> False

  -- THE REASON YOU DID THIS IS TO PREVENT FULLY ATTACHED LINES FROM BEING MOVED (because then thei'r endpoints jump around when the attached objects are destroyed)
  -- TODO FIX THE ABOVE
  -- I forgot why I added this, but in any case, we definitely DO want to transform lines if all it's attached parents are also being transformed
  --OwlSubItemLine sline -> not $
  --  (fromMaybe False $ _sAutoLine_attachStart sline <&> (\att -> hasOwlTree_exists ot (_attachment_target att)))
  --  && (fromMaybe False $ _sAutoLine_attachEnd sline <&> (\att -> hasOwlTree_exists ot (_attachment_target att)))

  
  _ -> True

-- TODO you MAY want to consider adding all lines that have both attachments in the selection to the modify set 
transformableSelection :: PotatoHandlerInput -> Seq SuperOwl
transformableSelection PotatoHandlerInput {..} = transformableSelection' _potatoHandlerInput_pFState _potatoHandlerInput_canvasSelection

transformableSelection' :: OwlPFState -> CanvasSelection -> Seq SuperOwl
transformableSelection' pfs sel = Seq.filter (flip superOwl_isTransformable pfs) (unCanvasSelection sel)


-- TODO rework this stuff, it was written with old assumptions that don't make sense anymore
data MouseManipulatorType = MouseManipulatorType_Corner | MouseManipulatorType_Side | MouseManipulatorType_Point | MouseManipulatorType_Area | MouseManipulatorType_Text deriving (Show, Eq)
data MouseManipulator = MouseManipulator {
  _mouseManipulator_box    :: LBox
  , _mouseManipulator_type :: MouseManipulatorType
  -- back reference to object being manipulated?
  -- or just use a function
}
type MouseManipulatorSet = [MouseManipulator]
type ManipulatorIndex = Int

toMouseManipulators :: OwlPFState -> CanvasSelection -> MouseManipulatorSet
toMouseManipulators pfs selection' = bb where
  union_lBoxes :: NonEmpty LBox -> LBox
  union_lBoxes (x:|xs) = foldl' union_lBox x xs
  selection = transformableSelection' pfs selection'
  fmapfn sowl = _sEltDrawer_box (getDrawer . hasOwlItem_toOwlSubItem $ sowl) pfs
  -- consider filtering out boxes with 0 area, but really _sEltDrawer_box should have return type Maybe LBox
  sboxes = toList $ fmap fmapfn selection
  bb = case sboxes of
    []   -> []
    x:xs -> fmap (flip makeHandleBox (union_lBoxes (x:|xs))) [BH_TL .. BH_A]

findFirstMouseManipulator :: OwlPFState -> RelMouseDrag -> CanvasSelection -> Maybe ManipulatorIndex
findFirstMouseManipulator pfs (RelMouseDrag MouseDrag {..}) selection = r where
  mms = toMouseManipulators pfs selection
  smt = computeSelectionType selection

  -- TODO use select magic here
  normalSel = L.findIndex (\mm -> does_lBox_contains_XY (_mouseManipulator_box mm) _mouseDrag_from) mms
  r = case smt of
    SMTTextArea -> normalSel -- TODO figure out how to differentiate between area / text manipulator
    _       -> normalSel


-- order is manipulator index
data BoxHandleType = BH_TL | BH_TR | BH_BL | BH_BR | BH_A | BH_T | BH_B | BH_L | BH_R  deriving (Show, Eq, Enum)

makeHandleBox ::
  BoxHandleType
  -> LBox -- ^ box being manipulated
  -> MouseManipulator
makeHandleBox bht (LBox (V2 x y) (V2 w h)) = case bht of
  BH_BR -> MouseManipulator box MouseManipulatorType_Corner
  BH_TL -> MouseManipulator box MouseManipulatorType_Corner
  BH_TR -> MouseManipulator box MouseManipulatorType_Corner
  BH_BL -> MouseManipulator box MouseManipulatorType_Corner
  BH_A  -> MouseManipulator box MouseManipulatorType_Area
  _     -> MouseManipulator box MouseManipulatorType_Side
  where
    (px, py) = (0,0) -- pan position
    CanonicalLBox _ _ clbox = canonicalLBox_from_lBox $ LBox (V2 (x+px) (y+py)) (V2 w h)
    nudgex = if w < 0 then 1 else 0
    nudgey = if h < 0 then 1 else 0
    l = x+px-1 + nudgex
    t = y+py-1 + nudgey
    r = x+px+w - nudgex
    b = y+py+h - nudgey
    box = case bht of
      BH_BR -> LBox (V2 r b) (V2 1 1)
      BH_TL -> LBox (V2 l t) (V2 1 1)
      BH_TR -> LBox (V2 r t) (V2 1 1)
      BH_BL -> LBox (V2 l b) (V2 1 1)
      BH_A  -> clbox
      _     -> error "not supported yet"

makeDeltaBox :: BoxHandleType -> XY -> DeltaLBox
makeDeltaBox bht (V2 dx dy) = case bht of
  BH_BR -> DeltaLBox 0 $ V2 dx dy
  BH_TL -> DeltaLBox (V2 dx dy) (V2 (-dx) (-dy))
  BH_TR -> DeltaLBox (V2 0 dy) (V2 dx (-dy))
  BH_BL -> DeltaLBox (V2 dx 0) (V2 (-dx) dy)
  BH_T  -> DeltaLBox (V2 0 dy) (V2 0 (-dy))
  BH_B  -> DeltaLBox 0 (V2 0 dy)
  BH_L  -> DeltaLBox (V2 dx 0) (V2 (-dx) 0)
  BH_R  -> DeltaLBox 0 (V2 dx 0)
  BH_A  -> DeltaLBox (V2 dx dy) (V2 0 0)



-- TODO rename to BoxHandlerType or something
data BoxCreationType = BoxCreationType_None | BoxCreationType_Box | BoxCreationType_Text | BoxCreationType_TextArea | BoxCreationType_DragSelect deriving (Show, Eq)

boxCreationType_isCreation :: BoxCreationType -> Bool
boxCreationType_isCreation bct = bct /= BoxCreationType_None && bct /= BoxCreationType_DragSelect


-- new handler stuff
data BoxHandler = BoxHandler {

    _boxHandler_handle      :: BoxHandleType -- the current handle we are dragging

    -- TODO this is wrong as makeDragOperation does not always return a Llama
    -- rename this to mouseActive or something
    , _boxHandler_undoFirst :: Bool

    -- with this you can use same code for both create and manipulate (create the handler and immediately pass input to it)
    , _boxHandler_creation  :: BoxCreationType
    , _boxHandler_active    :: Bool

    , _boxHandler_downOnLabel :: Bool


    , _boxHandler_prevDeltaLBox :: Maybe DeltaLBox

  } deriving (Show)

makeDragDeltaBox :: BoxHandleType -> RelMouseDrag -> DeltaLBox
makeDragDeltaBox bht rmd = r where
  RelMouseDrag MouseDrag {..} = rmd
  dragDelta = _mouseDrag_to - _mouseDrag_from
  shiftClick = elem KeyModifier_Shift _mouseDrag_modifiers

  boxRestrictedDelta = if shiftClick
    then restrict8 dragDelta
    else dragDelta

  r = makeDeltaBox bht boxRestrictedDelta

-- reduces the DeltaLBox such that the LBox does not invert
-- assumes LBox is canonical and that LBox is not already smaller than the desired constrained size
constrainDeltaLBox :: Int -> DeltaLBox -> LBox -> DeltaLBox
constrainDeltaLBox minsize d1@(DeltaLBox (V2 dx dy) (V2 dw dh)) (LBox _ (V2 w h)) = r where
  optuple e = (e, -e)

  (ndx, ndw) = if dx /= 0 
    then optuple (min (w-minsize) dx)
    else (dx, (max minsize (w+dw)) - w)
  
  (ndy, ndh) = if dy /= 0
    then optuple (min (h-minsize) dy)
    else (dy, (max minsize (h+dh)) - h)

  istranslateonly = dw == 0 && dh == 0
  
  r = if istranslateonly 
    then d1 
    else DeltaLBox (V2 ndx ndy) (V2 ndw ndh)

-- OR you remove the delta portion that already modified the box in preview
makeDragOperationNew :: PotatoHandlerInput -> DeltaLBox -> Maybe Llama
makeDragOperationNew phi dbox = op where
  selection = transformableSelection phi
  selectionl = toList $ transformableSelection phi
  pfs = _potatoHandlerInput_pFState phi
  lboxes = fmap (\sowl -> _sEltDrawer_box (getDrawer . hasOwlItem_toOwlSubItem $ sowl) pfs) selectionl

  -- go through each element in selection and ensure that dbox does not invert that element
  -- DANGER you need to make sure you have sensible bounding box functions or you might put things in a non-resizeable state
  constraineddbox = foldl' (constrainDeltaLBox 1) dbox lboxes

  fmapfn sowl = makeSetLlama (rid, newselt) where
    rid = _superOwl_id sowl
    oldselt = superOwl_toSElt_hack sowl
    -- TODO don't use the CBoundingBox version of that funciton, it's deprecated, write a new one.
    newselt = modify_sElt_with_cBoundingBox True oldselt (CBoundingBox constraineddbox)

  op = if Seq.null selection
    then Nothing
    else Just $ makeCompositionLlama . toList $ (fmap fmapfn selectionl)


makeDragOperation :: PotatoHandlerInput -> DeltaLBox -> Maybe Llama
makeDragOperation phi dbox = op where
  selection = transformableSelection phi
  selectionl = toList $ transformableSelection phi
  
  
  -- go through each element in selection and ensure that dbox does not invert that element
  -- DANGER you need to make sure you have sensible bounding box functions or you might put things in a non-resizeable state
  --pfs = _potatoHandlerInput_pFState phi
  --lboxes = fmap (\sowl -> _sEltDrawer_box (getDrawer . hasOwlItem_toOwlSubItem $ sowl) pfs) selectionl
  --constraineddbox = foldl' (constrainDeltaLBox 0) dbox lboxes

  makeController _ = cmd where
    cmd = CTagBoundingBox :=> (Identity $ CBoundingBox {
      _cBoundingBox_deltaBox = dbox -- constraineddbox
    })

  op = if Seq.null selection
    then Nothing
    else Just $ makePFCLlama . OwlPFCManipulate $ IM.fromList (fmap (\s -> (_superOwl_id s, makeController s)) selectionl)

-- TODO split this handler in two handlers
-- one for resizing selection (including boxes)
-- and one exclusively for creating boxes
instance Default BoxHandler where
  def = BoxHandler {
      _boxHandler_handle       = BH_BR
      , _boxHandler_undoFirst  = False
      , _boxHandler_creation = BoxCreationType_None
      , _boxHandler_active = False
      , _boxHandler_downOnLabel = False
      , _boxHandler_prevDeltaLBox = Nothing
      -- TODO whatever
      --, _boxHandler_wasDragged = False
    }



selectionOnlySBox :: CanvasSelection -> Maybe SBox
selectionOnlySBox (CanvasSelection selection) = if Seq.length selection == 1
  then case superOwl_toSElt_hack (Seq.index selection 0) of
    SEltBox sbox -> Just sbox
    _ -> Nothing
  else Nothing


isMouseOnSelectionSBoxBorder :: CanvasSelection -> RelMouseDrag -> Bool
isMouseOnSelectionSBoxBorder cs (RelMouseDrag MouseDrag {..}) = case selectionOnlySBox cs of
  -- not an SBox selected
  Nothing -> False
  Just sbox -> if sBoxType_hasBorder (_sBox_boxType sbox) && does_lBox_contains_XY (lBox_to_boxLabelBox (_sBox_box sbox)) _mouseDrag_from
    then True
    else False

minusDeltaLBox :: DeltaLBox -> DeltaLBox -> DeltaLBox
minusDeltaLBox (DeltaLBox (V2 dx1 dy1) (V2 dw1 dh1)) (DeltaLBox (V2 dx2 dy2) (V2 dw2 dh2)) = DeltaLBox (V2 (dx1-dx2) (dy1-dy2)) (V2 (dw1-dw2) (dh1-dh2))

instance PotatoHandler BoxHandler where
  pHandlerName _ = handlerName_box
  pHandleMouse bh@BoxHandler {..} phi@PotatoHandlerInput {..} rmd@(RelMouseDrag MouseDrag {..}) = case _mouseDrag_state of

    -- TODO creation should be a separate handler
    MouseDragState_Down | boxCreationType_isCreation _boxHandler_creation ->  Just $ def {
        _potatoHandlerOutput_nextHandler = Just $ SomePotatoHandler bh { _boxHandler_active = True }
      }
    -- if shift is held down, ignore inputs, this allows us to shift + click to deselect
    -- TODO consider moving this into GoatWidget since it's needed by many manipulators
    MouseDragState_Down | elem KeyModifier_Shift _mouseDrag_modifiers -> Nothing
    -- in DragSelect case we already have a selection
    MouseDragState_Down | _boxHandler_creation == BoxCreationType_DragSelect  -> assert (not . isParliament_null $ _potatoHandlerInput_selection) r where
        newbh = bh {
            -- drag select case is always BH_A
            _boxHandler_handle = BH_A
            , _boxHandler_active = True
          }
        r = Just def { _potatoHandlerOutput_nextHandler = Just $ SomePotatoHandler newbh }
    MouseDragState_Down -> case findFirstMouseManipulator _potatoHandlerInput_pFState rmd _potatoHandlerInput_canvasSelection of
      Nothing -> Nothing



      -- clicked on a manipulator, begin dragging
      Just mi -> r where
        newbh = bh {
            _boxHandler_handle = bht
            , _boxHandler_active = True
            -- label position always intersects BH_A so we do the test in here to see if we clicked on the label area
            , _boxHandler_downOnLabel = if bht == BH_A then isMouseOnSelectionSBoxBorder _potatoHandlerInput_canvasSelection rmd else False
          }
        bht = toEnum mi
        -- special case behavior for BH_A require actually clicking on something on selection
        clickOnSelection = any (doesSEltIntersectPoint _mouseDrag_to . superOwl_toSElt_hack) $ unCanvasSelection _potatoHandlerInput_canvasSelection
        r = if bht /= BH_A || clickOnSelection
          then Just def { _potatoHandlerOutput_nextHandler = Just $ SomePotatoHandler newbh }
          else Nothing


    MouseDragState_Dragging -> Just r where
      dragDelta = _mouseDrag_to - _mouseDrag_from
      newEltPos = lastPositionInSelection (_owlPFState_owlTree _potatoHandlerInput_pFState) _potatoHandlerInput_selection

      -- TODO do I use this for box creation? Prob want to restrictDiag or something though
      --shiftClick = elem KeyModifier_Shift _mouseDrag_modifiers
      --boxRestrictedDelta = if shiftClick then restrict8 dragDelta else dragDelta

      boxToAdd = def {
          _sBox_box     = canonicalLBox_from_lBox_ $ LBox _mouseDrag_from dragDelta
          -- consider using _potatoDefaultParameters_boxType instead
          , _sBox_boxType  = if _boxHandler_creation == BoxCreationType_Text
            then SBoxType_BoxText -- TODO pull from params
            else SBoxType_Box
          , _sBox_superStyle = _potatoDefaultParameters_superStyle _potatoHandlerInput_potatoDefaultParameters
          , _sBox_title = def { _sBoxTitle_align = _potatoDefaultParameters_box_label_textAlign _potatoHandlerInput_potatoDefaultParameters }
          , _sBox_text = def { _sBoxText_style = def { _textStyle_alignment = _potatoDefaultParameters_box_text_textAlign _potatoHandlerInput_potatoDefaultParameters } }
        }

      textAreaToAdd = def {
          _sTextArea_box   =  canonicalLBox_from_lBox_ $ LBox _mouseDrag_from dragDelta
          , _sTextArea_text        = Map.empty
          , _sTextArea_transparent = True
        }

      nameToAdd = case _boxHandler_creation of
        BoxCreationType_Box -> "<box>"
        BoxCreationType_Text -> "<text>"
        BoxCreationType_TextArea -> "<textarea>"
        _ -> error "invalid BoxCreationType"

      mdd = makeDragDeltaBox _boxHandler_handle rmd

      mop = case _boxHandler_creation of
        x | x == BoxCreationType_Box || x == BoxCreationType_Text -> Just $ makeAddEltLlama _potatoHandlerInput_pFState newEltPos (OwlItem (OwlInfo nameToAdd) (OwlSubItemBox boxToAdd))
        BoxCreationType_TextArea -> Just $ makeAddEltLlama _potatoHandlerInput_pFState newEltPos (OwlItem (OwlInfo nameToAdd) (OwlSubItemTextArea textAreaToAdd))
        _ -> makeDragOperationNew phi (minusDeltaLBox mdd (fromMaybe (DeltaLBox 0 0) _boxHandler_prevDeltaLBox))

      newbh = bh {
          _boxHandler_undoFirst = True
          -- if we drag, we are no longer in label case
          , _boxHandler_downOnLabel = False
          , _boxHandler_prevDeltaLBox = Just mdd
        }

      -- NOTE, that if we did create a new box, it wil get auto selected and a new BoxHandler will be created for it

      r = def {
          _potatoHandlerOutput_nextHandler = Just $ SomePotatoHandler newbh
          , _potatoHandlerOutput_action = case mop of
            Nothing -> HOA_Nothing
            Just op -> HOA_Preview $ Preview (previewOperation_fromUndoFirst _boxHandler_undoFirst) op
        }

    MouseDragState_Up | _boxHandler_downOnLabel -> if isMouseOnSelectionSBoxBorder _potatoHandlerInput_canvasSelection rmd
      -- clicked on the box label area
      -- pass on mouse as MouseDragState_Down is a hack but whatever it works
      -- TODO fix this hack, just have mouse up handle selection in this special case
      then pHandleMouse (makeBoxLabelHandler (SomePotatoHandler (def :: BoxHandler)) _potatoHandlerInput_canvasSelection rmd) phi rmd
      else Nothing
    MouseDragState_Up -> r where

      -- TODO do selectMagic here so we can enter text edit modes from multi-selections (you will also need to modify the selection)
      nselected = Seq.length (unCanvasSelection _potatoHandlerInput_canvasSelection)
      selt = superOwl_toSElt_hack <$> selectionToMaybeFirstSuperOwl _potatoHandlerInput_canvasSelection
      isBox = nselected == 1 && case selt of
        Just (SEltBox _) -> True
        _                                    -> False
      isText = nselected == 1 && case selt of
        Just (SEltBox SBox{..}) -> sBoxType_isText _sBox_boxType
        _                                    -> False
      isTextArea = nselected == 1 && case selt of
        Just (SEltTextArea _) -> True
        _ -> False


      -- only enter sub handler if we weren't drag selecting (this includes selecting it from an unselect state without dragging)
      wasNotDragSelecting = not (_boxHandler_creation == BoxCreationType_DragSelect)
      -- only enter subHandler we did not drag (hack, we do this by testing form _boxHandler_undoFirst)
      wasNotActuallyDragging = not _boxHandler_undoFirst
      -- always go straight to handler after creating a new SElt
      isCreation = boxCreationType_isCreation _boxHandler_creation
      r = if (isText || (isBox && not isCreation))
          && (wasNotActuallyDragging || isCreation)
          && wasNotDragSelecting
        -- create box handler and pass on the input (if it was not a text box it will be converted to one by the BoxTextHandler)
        then pHandleMouse (makeBoxTextHandler isCreation (SomePotatoHandler (def :: BoxHandler)) _potatoHandlerInput_canvasSelection rmd) phi rmd

        else if isTextArea
          && (wasNotActuallyDragging || isCreation)
          && wasNotDragSelecting
          then textAreaHandler_pHandleMouse_onCreation (makeTextAreaHandler (SomePotatoHandler (def :: BoxHandler)) _potatoHandlerInput_canvasSelection rmd isCreation) phi rmd

          -- This clears the handler and causes selection to regenerate a new handler.
          -- Why do we do it this way instead of returning a handler? Not sure, doesn't matter.
          else Just def {
              _potatoHandlerOutput_action = HOA_Preview Preview_MaybeCommit
              -- doesn't work, see comments where _boxHandler_undoFirst is defined
              --_potatoHandlerOutput_action = if _boxHandler_undoFirst then HOA_Preview Preview_Commit else HOA_Nothing
            }

        -- TODO if this was a text box creation case, consider entering text edit mode

      -- TODO consider handling special case, handle when you click and release create a box in one spot, create a box that has size 1 (rather than 0 if we did it during MouseDragState_Down normal way)

    MouseDragState_Cancelled -> if _boxHandler_undoFirst 
      then Just def { 
          -- you may or may not want to do this?
          --_potatoHandlerOutput_nextHandler = Just $ SomePotatoHandler (def :: BoxHandler)
          _potatoHandlerOutput_action = HOA_Preview Preview_Cancel 
        } 
      else Just def


  pHandleKeyboard bh phi@PotatoHandlerInput {..} (KeyboardData key _) = r where

    todlbox (x,y) = Just $ DeltaLBox (V2 x y) 0
    mmove = case key of
      KeyboardKey_Left -> todlbox (-1,0)
      KeyboardKey_Right -> todlbox (1,0)
      KeyboardKey_Up -> todlbox (0,-1)
      KeyboardKey_Down -> todlbox (0,1)
      _ -> Nothing

    r = if _boxHandler_active bh
      -- ignore inputs when we're in the middle of dragging
      then Nothing
      else case mmove of
        Nothing -> Nothing
        Just move -> Just r2 where
          mop = makeDragOperationNew phi move
          r2 = def {
              _potatoHandlerOutput_nextHandler = Just $ SomePotatoHandler bh
              , _potatoHandlerOutput_action = case mop of
                Nothing -> HOA_Nothing

                -- TODO we want to PO_Start/Continue here, but we need to Preview_Commit somewhere
                Just op -> HOA_Preview $ Preview PO_StartAndCommit op
            }

  pRenderHandler BoxHandler {..} PotatoHandlerInput {..} = r where
    handlePoints = fmap _mouseManipulator_box . filter (\mm -> _mouseManipulator_type mm == MouseManipulatorType_Corner) $ toMouseManipulators _potatoHandlerInput_pFState _potatoHandlerInput_canvasSelection
    -- TODO highlight active manipulator if active
    --if (_boxHandler_active)
    r = if not _boxHandler_active && boxCreationType_isCreation _boxHandler_creation
      -- don't render anything if we are about to create a box
      then emptyHandlerRenderOutput
      else HandlerRenderOutput (fmap defaultRenderHandle handlePoints)
      
  pIsHandlerActive bh = if _boxHandler_active bh then HAS_Active_Mouse else HAS_Inactive

  pHandlerTool BoxHandler {..} = case _boxHandler_creation of
    BoxCreationType_Box -> Just Tool_Box
    BoxCreationType_Text -> Just Tool_Text
    BoxCreationType_TextArea -> Just Tool_TextArea
    _ -> Nothing