-- 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.OwlWorkspace 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 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 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)) d2@((LBox (V2 x y) (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 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 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 -> "" BoxCreationType_Text -> "" BoxCreationType_TextArea -> "