brush list view: first steps

This commit is contained in:
sheaf 2024-10-09 16:57:08 +02:00
parent b5b29f124a
commit c1d6dd4151
9 changed files with 481 additions and 185 deletions

View file

@ -241,6 +241,7 @@ executable MetaBrush
, MetaBrush.Render.Document , MetaBrush.Render.Document
, MetaBrush.Render.Rulers , MetaBrush.Render.Rulers
, MetaBrush.Time , MetaBrush.Time
, MetaBrush.UI.BrushList
, MetaBrush.UI.Coordinates , MetaBrush.UI.Coordinates
, MetaBrush.UI.FileBar , MetaBrush.UI.FileBar
, MetaBrush.UI.InfoBar , MetaBrush.UI.InfoBar

View file

@ -522,7 +522,7 @@ indent {
color: black; color: black;
} }
.layer-item { .layer-item, .brush-item {
color: @plain; color: @plain;
background-color: @active; background-color: @active;
border: 0px solid @bg; border: 0px solid @bg;
@ -536,7 +536,7 @@ indent {
padding-left: 3px; padding-left: 3px;
} }
:selected .layer-item { :selected .layer-item, :selected .brush-item {
color: black; color: black;
background-color: @contrast; background-color: @contrast;
border: 0px solid @contrast; border: 0px solid @contrast;
@ -550,22 +550,33 @@ indent {
.drag-over.layer-item { .drag-over.layer-item {
} }
/* Style when dragging over the top part of an item */ /* Style when dragging layer item over the top part of an item */
.drag-top.layer-item { .dragging-item .drag-top.layer-item {
border-top: 2px solid @highlight; border-top: 2px solid @highlight;
margin-top: -2px; margin-top: -2px;
box-shadow: box-shadow:
inset 0 8px 6px -6px @highlight; inset 0 8px 6px -6px @highlight;
} }
/* Style when dragging over the bottom part of an item */ /* Style when dragging layer item over the bottom part of an item */
.drag-bot.layer-item { .dragging-item .drag-bot.layer-item {
border-bottom: 2px solid @highlight; border-bottom: 2px solid @highlight;
margin-bottom: -2px; margin-bottom: -2px;
box-shadow: box-shadow:
inset 0 -8px 6px -6px @highlight; inset 0 -8px 6px -6px @highlight;
} }
/* Style when dragging brush over an item */
.dragging-brush .drag-top.layer-item, .dragging-brush .drag-bot.layer-item {
border-top: 2px solid @brushStroke;
border-bottom: 2px solid @brushStroke;
border-left: 0px;
border-right: 0px;
margin-top: -2px;
margin-bottom: -2px;
background-color: @brushStroke;
}
/* Style for item being dragged */ /* Style for item being dragged */
.dragged.layer-item { .dragged.layer-item {
background-color: @bg; background-color: @bg;

View file

@ -105,6 +105,8 @@ import MetaBrush.Render.Document
import MetaBrush.Render.Rulers import MetaBrush.Render.Rulers
( renderRuler ) ( renderRuler )
import MetaBrush.Stroke import MetaBrush.Stroke
import MetaBrush.UI.BrushList
( newBrushView )
import MetaBrush.UI.FileBar import MetaBrush.UI.FileBar
( FileBar(..), createFileBar ) ( FileBar(..), createFileBar )
import MetaBrush.UI.InfoBar import MetaBrush.UI.InfoBar
@ -113,7 +115,7 @@ import MetaBrush.UI.Menu
( createMenuBar, createMenuActions ) ( createMenuBar, createMenuActions )
import MetaBrush.UI.Panels import MetaBrush.UI.Panels
import MetaBrush.UI.StrokeTreeView import MetaBrush.UI.StrokeTreeView
( newLayerView ) ( DragSourceData(..), newLayerView )
import MetaBrush.UI.ToolBar import MetaBrush.UI.ToolBar
( Tool(..), Mode(..), createToolBar ) ( Tool(..), Mode(..), createToolBar )
import MetaBrush.UI.Viewport import MetaBrush.UI.Viewport
@ -474,6 +476,14 @@ runApplication application = do
( layersScrolledWindow panelsBar ) ( layersScrolledWindow panelsBar )
( Just strokesListView ) ( Just strokesListView )
---------------------------------------------------------
-- Brushes view
brushesListView <- newBrushView window DragBrush
GTK.scrolledWindowSetChild
( brushesScrolledWindow panelsBar )
( Just brushesListView )
GTK.boxAppend mainView fileBarBox GTK.boxAppend mainView fileBarBox
GTK.boxAppend mainView viewportGrid GTK.boxAppend mainView viewportGrid
GTK.boxAppend mainView infoBarArea GTK.boxAppend mainView infoBarArea

View file

@ -0,0 +1,190 @@
{-# LANGUAGE ImplicitParams #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecursiveDo #-}
module MetaBrush.UI.BrushList
( newBrushListModel
, newBrushView
, getSelectedBrush
)
where
-- base
import Control.Monad
( void )
import Data.IORef
( newIORef, atomicWriteIORef, readIORef )
import Data.Maybe
( fromMaybe )
import Data.Typeable
( Typeable )
-- gi-gdk
import qualified GI.Gdk as GDK
-- gi-gio
import qualified GI.Gio as GIO
-- gi-gtk
import qualified GI.Gtk as GTK
-- haskell-gi-base
import qualified Data.GI.Base as GI
import qualified Data.GI.Base.GValue as GI
-- text
import Data.Text
( Text )
import qualified Data.Text as Text
( toLower )
-- unordered-containers
import qualified Data.HashMap.Strict as HashMap
-- MetaBrush
import qualified MetaBrush.Asset.Brushes as Brushes
( brushes )
--------------------------------------------------------------------------------
-- GTK StringList --
--------------------
-- | Create a new 'GTK.StringList' with 'GTK.SingleSelection' to hold
-- the given brush names.
newBrushListModel :: [ Text ] -> IO GTK.SingleSelection
newBrushListModel brushNames = do
stringList <- GTK.stringListNew ( Just brushNames )
selectionModel <- GI.withManagedPtr stringList $ \ slPtr ->
GI.withNewObject slPtr $ \ slCopy ->
GTK.singleSelectionNew ( Just slCopy )
return selectionModel
------------------
-- GTK ListView --
------------------
-- | Create a new 'GTK.ListView' that displays brush names.
newBrushView :: Typeable dnd_data
=> GTK.ApplicationWindow
-> ( Maybe Text -> dnd_data )
-- ^ drag and drop data for the brush
-> IO GTK.ListView
newBrushView rootWindow mkDND_data = mdo
brushListModel <- newBrushListModel ( "No brush" : HashMap.keys Brushes.brushes )
brushesListFactory <- GTK.signalListItemFactoryNew
dragPosRef <- newIORef ( 0, 0 )
-- Connect to "setup" signal to create generic widgets for viewing brush names.
_ <- GTK.onSignalListItemFactorySetup brushesListFactory $ \ listItem0 -> do
listItem <- GTK.unsafeCastTo GTK.ListItem listItem0
GTK.listItemSetFocusable listItem False
brushBox <- GTK.boxNew GTK.OrientationHorizontal 10
GTK.listItemSetChild listItem ( Just brushBox )
GTK.widgetAddCssClass brushBox "brush-item"
brushLabel <- GTK.labelNew ( Nothing @Text )
GTK.boxAppend brushBox brushLabel
----------------
-- DragSource --
----------------
-- Connect signals for starting a drag from this widget.
dragSource <- GTK.dragSourceNew
GTK.dragSourceSetActions dragSource [ GDK.DragActionCopy ]
void $ GTK.onDragSourcePrepare dragSource $ \ x y -> do
atomicWriteIORef dragPosRef ( x, y )
brushName <- getBrushName listItem
val <- GDK.contentProviderNewForValue =<< GIO.toGValue ( GI.HValue $ mkDND_data brushName )
GTK.widgetAddCssClass rootWindow "dragging-brush"
return $ Just val
void $ GTK.onDragSourceDragBegin dragSource $ \ _drag -> do
( x, y ) <- readIORef dragPosRef
paintable <- GTK.widgetPaintableNew ( Just brushBox )
GTK.dragSourceSetIcon ?self ( Just paintable ) ( round x ) ( round y )
GTK.widgetAddCssClass brushBox "dragged-brush"
-- TODO: add "dragged" class for all descendants as well.
void $ GTK.onDragSourceDragCancel dragSource $ \ _drag _reason -> do
GTK.widgetRemoveCssClass rootWindow "dragging-brush"
GTK.widgetRemoveCssClass brushBox "dragged-brush"
return True
-- ^^^^ Important. Setting this to 'False' stops GDK
-- from properly clearing the drag cursor.
void $ GTK.onDragSourceDragEnd dragSource $ \ _drag _deleteData -> do
GTK.widgetRemoveCssClass rootWindow "dragging-brush"
GTK.widgetRemoveCssClass brushBox "dragged-brush"
GTK.widgetAddController brushBox dragSource
-- Connect to "bind" signal to modify the generic widget to display the data for this list item.
_ <- GTK.onSignalListItemFactoryBind brushesListFactory $ \ listItem0 -> do
listItem <- GTK.unsafeCastTo GTK.ListItem listItem0
brushName <- getBrushName listItem
mbBrushBox <- GTK.listItemGetChild listItem
brushBox <-
case mbBrushBox of
Nothing -> error "brushList onBind: list item has no child"
Just box0 -> GTK.unsafeCastTo GTK.Box box0
mbBrushLabel <- GTK.widgetGetFirstChild brushBox
brushLabel <-
case mbBrushLabel of
Nothing -> error "brushList onBind: brush box has no child"
Just lbl0 -> GTK.unsafeCastTo GTK.Label lbl0
GTK.labelSetLabel brushLabel ( fromMaybe "no brush" brushName )
listView <- GTK.listViewNew ( Nothing @GTK.SingleSelection ) ( Just brushesListFactory )
GTK.listViewSetModel listView ( Just brushListModel )
return listView
getBrushName :: GTK.ListItem -> IO ( Maybe Text )
getBrushName listItem = do
mbStrObj <- GTK.listItemGetItem listItem
case mbStrObj of
Nothing -> error "getBrushName: ListItem has no item"
Just strObj0 -> do
strObj <- GTK.unsafeCastTo GTK.StringObject strObj0
str <- GTK.stringObjectGetString strObj
return $
if str == "" || Text.toLower str == "no brush"
then Nothing
else Just str
-- | Get the name of the selected brush in the 'ListView', if any.
getSelectedBrush :: GTK.ListView -> IO ( Maybe Text )
getSelectedBrush layersView = do
mbSelectionModel <- GTK.listViewGetModel layersView
case mbSelectionModel of
Nothing -> error "getSelectedBrush: no SelectionModel"
Just selModel0 -> do
selModel <- GTK.unsafeCastTo GTK.SingleSelection selModel0
mbRow <- GTK.singleSelectionGetSelectedItem selModel
case mbRow of
Nothing -> return Nothing
Just row0 -> do
row <- GTK.unsafeCastTo GTK.TreeListRow row0
mbItem <- GTK.treeListRowGetItem row
case mbItem of
Nothing -> error "getSelectedBrush: row has no item"
Just item0 -> do
strObj <- GTK.unsafeCastTo GTK.StringObject item0
str <- GTK.stringObjectGetString strObj
return $
if str == "" || Text.toLower str == "no brush"
then Nothing
else Just str

View file

@ -25,9 +25,9 @@ import MetaBrush.GTK.Util
data PanelsBar data PanelsBar
= PanelsBar = PanelsBar
{ layersScrolledWindow { layersScrolledWindow, brushesScrolledWindow
:: !GTK.ScrolledWindow :: !GTK.ScrolledWindow
, brushesPanelBox, transformPanelBox, historyPanelBox , transformPanelBox, historyPanelBox
:: !GTK.Box :: !GTK.Box
} }
@ -45,56 +45,55 @@ createPanelBar panelBox = do
panels1 <- GTK.notebookNew panels1 <- GTK.notebookNew
panels2 <- GTK.notebookNew panels2 <- GTK.notebookNew
GTK.notebookSetGroupName panels1 ( Just "Panel" ) GTK.notebookSetGroupName panels1 ( Just "Top panel" )
GTK.notebookSetGroupName panels2 ( Just "Panel" ) GTK.notebookSetGroupName panels2 ( Just "Bottom panel" )
GTK.panedSetStartChild pane1 ( Just panels1 ) GTK.panedSetStartChild pane1 ( Just panels1 )
GTK.panedSetEndChild pane1 ( Just panels2 ) GTK.panedSetEndChild pane1 ( Just panels2 )
layersScrolledWindow <- GTK.scrolledWindowNew layersScrolledWindow <- GTK.scrolledWindowNew
GTK.scrolledWindowSetPolicy layersScrolledWindow GTK.PolicyTypeNever GTK.PolicyTypeAutomatic GTK.scrolledWindowSetPolicy layersScrolledWindow GTK.PolicyTypeNever GTK.PolicyTypeAutomatic
brushesScrolledWindow <- GTK.scrolledWindowNew
GTK.scrolledWindowSetPolicy brushesScrolledWindow GTK.PolicyTypeNever GTK.PolicyTypeAutomatic
brushesPanelBox <- GTK.boxNew GTK.OrientationVertical 0
transformPanelBox <- GTK.boxNew GTK.OrientationVertical 0 transformPanelBox <- GTK.boxNew GTK.OrientationVertical 0
historyPanelBox <- GTK.boxNew GTK.OrientationVertical 0 historyPanelBox <- GTK.boxNew GTK.OrientationVertical 0
strokesTab <- GTK.labelNew ( Just "Strokes" ) strokesTab <- GTK.labelNew ( Just "Strokes" )
brushesTab <- GTK.labelNew ( Just "Brushes" ) brushesTab <- GTK.labelNew ( Just "Brushes" )
transformTab <- GTK.labelNew ( Just "Transform" ) transformTab <- GTK.labelNew ( Just "Transform" )
historyTab <- GTK.labelNew ( Just "History" ) historyTab <- GTK.labelNew ( Just "History" )
for_ [ strokesTab, brushesTab, transformTab, historyTab ] \ tab -> do for_ [ strokesTab, brushesTab, transformTab, historyTab ] \ tab -> do
widgetAddClasses tab [ "plain", "text", "panelTab" ] widgetAddClasses tab [ "plain", "text", "panelTab" ]
widgetAddClass layersScrolledWindow "panel" for_ [ layersScrolledWindow, brushesScrolledWindow ] \ w -> widgetAddClass w "panel"
for_ [ brushesPanelBox, transformPanelBox, historyPanelBox ] \ panel -> do for_ [ transformPanelBox, historyPanelBox ] \ panel -> do
widgetAddClass panel "panel" widgetAddClass panel "panel"
void $ GTK.notebookAppendPage panels1 layersScrolledWindow ( Just strokesTab ) void $ GTK.notebookAppendPage panels1 brushesScrolledWindow ( Just brushesTab )
void $ GTK.notebookAppendPage panels1 brushesPanelBox ( Just brushesTab ) void $ GTK.notebookAppendPage panels1 historyPanelBox ( Just historyTab )
void $ GTK.notebookAppendPage panels2 transformPanelBox ( Just transformTab ) void $ GTK.notebookAppendPage panels2 layersScrolledWindow ( Just strokesTab )
void $ GTK.notebookAppendPage panels2 historyPanelBox ( Just historyTab ) void $ GTK.notebookAppendPage panels2 transformPanelBox ( Just transformTab )
GTK.notebookSetTabReorderable panels1 layersScrolledWindow True GTK.notebookSetTabReorderable panels1 brushesScrolledWindow True
GTK.notebookSetTabDetachable panels1 layersScrolledWindow True GTK.notebookSetTabDetachable panels1 brushesScrolledWindow True
GTK.notebookSetTabReorderable panels1 brushesPanelBox True GTK.notebookSetTabReorderable panels1 historyPanelBox True
GTK.notebookSetTabDetachable panels1 brushesPanelBox True GTK.notebookSetTabDetachable panels1 historyPanelBox True
GTK.notebookSetTabReorderable panels2 transformPanelBox True GTK.notebookSetTabReorderable panels2 layersScrolledWindow True
GTK.notebookSetTabDetachable panels2 transformPanelBox True GTK.notebookSetTabDetachable panels2 layersScrolledWindow True
GTK.notebookSetTabReorderable panels2 historyPanelBox True GTK.notebookSetTabReorderable panels2 transformPanelBox True
GTK.notebookSetTabDetachable panels2 historyPanelBox True GTK.notebookSetTabDetachable panels2 transformPanelBox True
brushesContent <- GTK.labelNew ( Just "Brushes tab content..." )
transformContent <- GTK.labelNew ( Just "Transform tab content..." ) transformContent <- GTK.labelNew ( Just "Transform tab content..." )
historyContent <- GTK.labelNew ( Just "History tab content..." ) historyContent <- GTK.labelNew ( Just "History tab content..." )
GTK.boxAppend brushesPanelBox brushesContent
GTK.boxAppend transformPanelBox transformContent GTK.boxAppend transformPanelBox transformContent
GTK.boxAppend historyPanelBox historyContent GTK.boxAppend historyPanelBox historyContent
return $ return $
PanelsBar { layersScrolledWindow PanelsBar { layersScrolledWindow, brushesScrolledWindow
, brushesPanelBox, transformPanelBox, historyPanelBox , transformPanelBox, historyPanelBox
} }

View file

@ -67,12 +67,14 @@ import Data.Text
-- MetaBrush -- MetaBrush
import MetaBrush.Application.Context import MetaBrush.Application.Context
import MetaBrush.Application.UpdateDocument import MetaBrush.Application.UpdateDocument
import MetaBrush.Asset.Brushes
( lookupBrush )
import MetaBrush.Brush
import MetaBrush.Document import MetaBrush.Document
import MetaBrush.Document.Diff import MetaBrush.Document.Diff
import MetaBrush.Document.History import MetaBrush.Document.History
import MetaBrush.Layer import MetaBrush.Layer
import MetaBrush.Stroke hiding ( Layer(..) ) import MetaBrush.Stroke hiding ( Layer(..) )
import MetaBrush.UI.Panels ( PanelsBar(..) )
import MetaBrush.Unique import MetaBrush.Unique
import MetaBrush.Util import MetaBrush.Util
( (!) ) ( (!) )
@ -102,6 +104,17 @@ instance GI.DerivedGObject LayerItem where
objectInstanceInit _ _ = return Nothing objectInstanceInit _ _ = return Nothing
objectInterfaces = [ ] objectInterfaces = [ ]
--------------------------------------------------------------------------------
-- Drag and drop data --
------------------------
-- | Data passed in a drag-and-drop operation associated with the drag source.
data DragSourceData
= DragBrush { draggedBrushName :: !( Maybe Text ) }
| DragLayerItem { draggedLayerItem :: !ChildLayer }
deriving stock ( Show, Eq )
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- GTK TreeListModel -- -- GTK TreeListModel --
----------------------- -----------------------
@ -386,7 +399,7 @@ getLayerViewWidget expander = do
-- | Create a new 'GTK.ListView' that displays 'LayerItem's. -- | Create a new 'GTK.ListView' that displays 'LayerItem's.
newLayerView :: UIElements -> Variables -> IO GTK.ListView newLayerView :: UIElements -> Variables -> IO GTK.ListView
newLayerView uiElts@( UIElements { panelsBar = PanelsBar { layersScrolledWindow } } ) vars = mdo newLayerView uiElts@( UIElements { window } ) vars = mdo
layersListFactory <- GTK.signalListItemFactoryNew layersListFactory <- GTK.signalListItemFactoryNew
@ -480,13 +493,14 @@ newLayerView uiElts@( UIElements { panelsBar = PanelsBar { layersScrolledWindow
GTK.singleSelectionSetSelected selModel rowPos GTK.singleSelectionSetSelected selModel rowPos
let dnd_sourceItem = let dnd_sourceItem =
WithinParent DragLayerItem $
{ parent = fmap snd srcPar WithinParent
, item = srcUniq { parent = fmap snd srcPar
} , item = srcUniq
}
val <- GDK.contentProviderNewForValue =<< GIO.toGValue ( GI.HValue dnd_sourceItem ) val <- GDK.contentProviderNewForValue =<< GIO.toGValue ( GI.HValue dnd_sourceItem )
GTK.widgetAddCssClass layersScrolledWindow "dragging-item" GTK.widgetAddCssClass window "dragging-item"
return $ Just val return $ Just val
void $ GTK.onDragSourceDragBegin dragSource $ \ _drag -> do void $ GTK.onDragSourceDragBegin dragSource $ \ _drag -> do
@ -501,13 +515,13 @@ newLayerView uiElts@( UIElements { panelsBar = PanelsBar { layersScrolledWindow
GTK.widgetAddCssClass expander "dragged" GTK.widgetAddCssClass expander "dragged"
-- TODO: add "dragged" class for all descendants as well. -- TODO: add "dragged" class for all descendants as well.
void $ GTK.onDragSourceDragCancel dragSource $ \ _drag _reason -> do void $ GTK.onDragSourceDragCancel dragSource $ \ _drag _reason -> do
GTK.widgetRemoveCssClass layersScrolledWindow "dragging-item" GTK.widgetRemoveCssClass window "dragging-item"
GTK.widgetRemoveCssClass expander "dragged" GTK.widgetRemoveCssClass expander "dragged"
return True return True
-- ^^^^ Important. Setting this to 'False' stops GDK -- ^^^^ Important. Setting this to 'False' stops GDK
-- from properly clearing the drag cursor. -- from properly clearing the drag cursor.
void $ GTK.onDragSourceDragEnd dragSource $ \ _drag _deleteData -> do void $ GTK.onDragSourceDragEnd dragSource $ \ _drag _deleteData -> do
GTK.widgetRemoveCssClass layersScrolledWindow "dragging-item" GTK.widgetRemoveCssClass window "dragging-item"
GTK.widgetRemoveCssClass expander "dragged" GTK.widgetRemoveCssClass expander "dragged"
---------------- ----------------
@ -532,147 +546,162 @@ newLayerView uiElts@( UIElements { panelsBar = PanelsBar { layersScrolledWindow
-- LayerID {} -> return True -- LayerID {} -> return True
void $ GTK.onDropTargetDrop dropTarget $ \ val _x y -> do void $ GTK.onDropTargetDrop dropTarget $ \ val _x y -> do
dropTargetCleanup dropTargetCleanup
dropTgtUniq <- layerUnique <$> getLayerData listItem dropTgt <- getLayerData listItem
GI.HValue dragSrc@( WithinParent { item = dragSrcUniq }) <- GI.HValue dnd_data <- GIO.fromGValue @( GI.HValue DragSourceData ) val
GIO.fromGValue @( GI.HValue ChildLayer ) val
mbTreeListRow <- traverse ( GTK.unsafeCastTo GTK.TreeListRow ) =<< GTK.listItemGetItem listItem mbTreeListRow <- traverse ( GTK.unsafeCastTo GTK.TreeListRow ) =<< GTK.listItemGetItem listItem
treeListRow <- case mbTreeListRow of treeListRow <- case mbTreeListRow of
Nothing -> error "newLayerView ListItem onSetup: no TreeListRow" Nothing -> error "newLayerView ListItem onSetup: no TreeListRow"
Just r -> return r Just r -> return r
dstFlatIndex <- GTK.treeListRowGetPosition treeListRow
h <- GTK.widgetGetHeight expander
let droppedAbove = y < 0.5 * fromIntegral h
expanded <- GTK.treeListRowGetExpanded treeListRow
dstPar <- getParent treeListRow dstPar <- getParent treeListRow
isDescendant <- isDescendantOf dragSrcUniq listItem
let mbDropIntoGroup case dnd_data of
| expanded DragBrush brushName -> do
, not droppedAbove case dropTgt of
, not isDescendant GroupLayer {} ->
= Just treeListRow return False
| otherwise StrokeLayer tgtStrokeUnique -> do
= Nothing updateLayerHierarchy vars $
mbDropOutsideGroup DoLayerChange $
| dragSrcUniq == dropTgtUniq SetBrush
, Parent par <- dstPar { setBrushStroke = WithinParent ( fmap snd dstPar ) tgtStrokeUnique
, not droppedAbove , setBrushName = brushName
= Just par
| otherwise
= Nothing
if isDescendant && isNothing mbDropOutsideGroup
then do
return False
else do
-- Compute the destination parent.
-- Usually, the destination parent is the parent of the drop target.
-- BUT:
-- 1. when dropping an item into the first position of an
-- expanded group, the destination parent is the drop target itself,
-- not the parent of the drop target.
-- 2. when an item is at the bottom of a group, dropping it on its
-- lower half moves the item out of the group, so the
-- destination parent is the grand-parent of the drop target.
( dropDst, newPosInTree ) <-
if
-- (1)
| Just dstParRow <- mbDropIntoGroup
-> do
dstParFlatIndex <- GTK.treeListRowGetPosition dstParRow
return $
( MoveToTopOfGroup dropTgtUniq
, dstParFlatIndex + 1
)
-- (2)
| Just ( dstParRow, dstParUniq ) <- mbDropOutsideGroup
-> do
grandPar <- getParent dstParRow
return $
( MoveItemOutsideGroupIfLastItemInGroup
{ itemUnique = dropTgtUniq
, parentUnique = dstParUniq
, grandParentUnique = fmap snd grandPar
, itemExpanded = expanded
}
, dstFlatIndex + 1
)
| otherwise
-> do
return $
( MoveAboveOrBelow
{ moveDstItem =
WithinParent
{ parent = fmap snd dstPar
, item = dropTgtUniq
}
, moveAbove = droppedAbove
} }
, if droppedAbove then dstFlatIndex else dstFlatIndex + 1 return True
)
-- Compute the position that the item we are moving will have DragLayerItem dragSrc@( WithinParent { item = dragSrcUniq }) -> do
-- at the end of the move. let dropTgtUniq = layerUnique dropTgt
-- dstFlatIndex <- GTK.treeListRowGetPosition treeListRow
-- First, we compute whether we moved up or down. h <- GTK.widgetGetHeight expander
-- NB: we need to compute the source item position now (using 'treeListRowGetPosition'), let droppedAbove = y < 0.5 * fromIntegral h
-- at the end of the drag-and-drop operation, because TreeExpander nodes expanded <- GTK.treeListRowGetExpanded treeListRow
-- might have expanded/collapsed in the meantime.
mbSelModel <- GTK.listViewGetModel listView
case mbSelModel of
Nothing -> return False
Just selModel0 -> do
selModel <- GTK.unsafeCastTo GTK.SingleSelection selModel0
layersListModel
<- GTK.unsafeCastTo GTK.TreeListModel =<< fmap fromJust ( GTK.singleSelectionGetModel selModel )
mbSelItem <- GTK.singleSelectionGetSelectedItem selModel isDescendant <- isDescendantOf dragSrcUniq listItem
mbSelIx <- for mbSelItem $ \ selItem -> do
selRow <- GTK.unsafeCastTo GTK.TreeListRow selItem
GTK.treeListRowGetPosition selRow
-- Now compute the final destination position. let mbDropIntoGroup
mbDstPosAfterShift <- | expanded
case mbSelIx of , not droppedAbove
Nothing -> , not isDescendant
return Nothing = Just treeListRow
Just selIx | otherwise
-- If we moved up, simply use the destination position. = Nothing
| selIx >= newPosInTree mbDropOutsideGroup
-> return $ Just newPosInTree | dragSrcUniq == dropTgtUniq
| otherwise , Parent par <- dstPar
-> do , not droppedAbove
-- If we moved down, we need to substract the number of items = Just par
-- moved. Note that this depends on which TreeExpander nodes | otherwise
-- are expanded. = Nothing
mbSelRow <- GTK.treeListModelGetRow layersListModel selIx
case mbSelRow of
Nothing -> return Nothing
Just selRow0 -> do
selRow <- GTK.unsafeCastTo GTK.TreeListRow selRow0
nbDescendants <- getNbExpandedDescendants layersListModel selRow
return $
if newPosInTree < nbDescendants
then Nothing
else Just $ newPosInTree - nbDescendants
updateLayerHierarchy vars $ if isDescendant && isNothing mbDropOutsideGroup
DoLayerChange $ then do
Move return False
{ moveSrc = dragSrc else do
, moveDst = dropDst -- Compute the destination parent.
} -- Usually, the destination parent is the parent of the drop target.
-- BUT:
-- 1. when dropping an item into the first position of an
-- expanded group, the destination parent is the drop target itself,
-- not the parent of the drop target.
-- 2. when an item is at the bottom of a group, dropping it on its
-- lower half moves the item out of the group, so the
-- destination parent is the grand-parent of the drop target.
( dropDst, newPosInTree ) <-
if
-- (1)
| Just dstParRow <- mbDropIntoGroup
-> do
dstParFlatIndex <- GTK.treeListRowGetPosition dstParRow
return $
( MoveToTopOfGroup dropTgtUniq
, dstParFlatIndex + 1
)
-- (2)
| Just ( dstParRow, dstParUniq ) <- mbDropOutsideGroup
-> do
grandPar <- getParent dstParRow
return $
( MoveItemOutsideGroupIfLastItemInGroup
{ itemUnique = dropTgtUniq
, parentUnique = dstParUniq
, grandParentUnique = fmap snd grandPar
, itemExpanded = expanded
}
, dstFlatIndex + 1
)
| otherwise
-> do
return $
( MoveAboveOrBelow
{ moveDstItem =
WithinParent
{ parent = fmap snd dstPar
, item = dropTgtUniq
}
, moveAbove = droppedAbove
}
, if droppedAbove then dstFlatIndex else dstFlatIndex + 1
)
-- After moving, update the selected item to be the moved item. -- Compute the position that the item we are moving will have
case mbDstPosAfterShift of -- at the end of the move.
Nothing -> return () --
Just dstPos -> -- First, we compute whether we moved up or down.
GTK.singleSelectionSetSelected selModel dstPos -- NB: we need to compute the source item position now (using 'treeListRowGetPosition'),
return True -- at the end of the drag-and-drop operation, because TreeExpander nodes
-- might have expanded/collapsed in the meantime.
mbSelModel <- GTK.listViewGetModel listView
case mbSelModel of
Nothing -> return False
Just selModel0 -> do
selModel <- GTK.unsafeCastTo GTK.SingleSelection selModel0
layersListModel
<- GTK.unsafeCastTo GTK.TreeListModel =<< fmap fromJust ( GTK.singleSelectionGetModel selModel )
mbSelItem <- GTK.singleSelectionGetSelectedItem selModel
mbSelIx <- for mbSelItem $ \ selItem -> do
selRow <- GTK.unsafeCastTo GTK.TreeListRow selItem
GTK.treeListRowGetPosition selRow
-- Now compute the final destination position.
mbDstPosAfterShift <-
case mbSelIx of
Nothing ->
return Nothing
Just selIx
-- If we moved up, simply use the destination position.
| selIx >= newPosInTree
-> return $ Just newPosInTree
| otherwise
-> do
-- If we moved down, we need to substract the number of items
-- moved. Note that this depends on which TreeExpander nodes
-- are expanded.
mbSelRow <- GTK.treeListModelGetRow layersListModel selIx
case mbSelRow of
Nothing -> return Nothing
Just selRow0 -> do
selRow <- GTK.unsafeCastTo GTK.TreeListRow selRow0
nbDescendants <- getNbExpandedDescendants layersListModel selRow
return $
if newPosInTree < nbDescendants
then Nothing
else Just $ newPosInTree - nbDescendants
updateLayerHierarchy vars $
DoLayerChange $
Move
{ moveSrc = dragSrc
, moveDst = dropDst
}
-- After moving, update the selected item to be the moved item.
case mbDstPosAfterShift of
Nothing -> return ()
Just dstPos ->
GTK.singleSelectionSetSelected selModel dstPos
return True
void $ GTK.onDropTargetEnter dropTarget $ \ _x y -> do void $ GTK.onDropTargetEnter dropTarget $ \ _x y -> do
GTK.widgetAddCssClass expander "drag-over" GTK.widgetAddCssClass expander "drag-over"
@ -880,6 +909,10 @@ data LayerChange
| Delete | Delete
{ deleteItem :: !ChildLayer { deleteItem :: !ChildLayer
} }
| SetBrush
{ setBrushStroke :: !ChildLayer
, setBrushName :: !( Maybe Text )
}
-- | Destination of a move operation. -- | Destination of a move operation.
data MoveDst data MoveDst
@ -1092,6 +1125,31 @@ applyChangeToLayerHierarchy change hierarchy =
, delIsGroup = isJust mbHadChildren , delIsGroup = isJust mbHadChildren
} }
) )
SetBrush { setBrushStroke = WithinParent parUniq strokeUnique, setBrushName } ->
let
oldStroke, newStroke :: Stroke
oldStroke = content hierarchy ! strokeUnique
newStroke = case setBrushName of
Just brushNm
| Just ( SomeBrush brush ) <- lookupBrush brushNm
-> setStrokeBrush ( Just brush ) oldStroke
_ -> setStrokeBrush Nothing oldStroke
strokeIx :: Word32
strokeIx =
let oldPar_cs = lookupChildren parUniq hierarchy
in fromIntegral $ fromJust $ elemIndex strokeUnique oldPar_cs
hierarchy' = hierarchy { content = Map.insert strokeUnique newStroke ( content hierarchy ) }
in
( hierarchy'
, Map.empty
, Just $
StrokeSetBrush
{ changedBrushStroke = WithinParent parUniq strokeIx
, newBrushName = setBrushName
}
)
-- | Apply a change to the 'ListModel' underlying the UI -- | Apply a change to the 'ListModel' underlying the UI
-- representation of the layer hierarchy. -- representation of the layer hierarchy.
@ -1148,6 +1206,9 @@ applyDiffToListModel parStoreTVar docUnique ( doOrUndo, diff ) = do
DeletePoints { deletedStrokes } -> DeletePoints { deletedStrokes } ->
unless ( null deletedStrokes ) $ unless ( null deletedStrokes ) $
putStrLn "TODO: delete strokes" putStrLn "TODO: delete strokes"
StrokeSetBrush { changedBrushStroke = WithinParent changedPar changedIx } -> do
let changedStrokeStore = parStoreFromUniq ! changedPar
GIO.listModelItemsChanged changedStrokeStore changedIx 0 0
-- | Update the 'StrokeHierarchy' after a drag-and-drop operation, -- | Update the 'StrokeHierarchy' after a drag-and-drop operation,
-- moving one layer or group around. -- moving one layer or group around.

View file

@ -13,6 +13,10 @@ import Data.Set
import Control.DeepSeq import Control.DeepSeq
( NFData(..) ) ( NFData(..) )
-- text
import Data.Text
( Text )
-- brush-strokes -- brush-strokes
import Math.Bezier.Spline import Math.Bezier.Spline
import Math.Linear import Math.Linear
@ -73,6 +77,10 @@ data HierarchyDiff
, srcPos :: !ChildLayerPosition , srcPos :: !ChildLayerPosition
, dstPos :: !ChildLayerPosition , dstPos :: !ChildLayerPosition
} }
| StrokeSetBrush
{ changedBrushStroke :: !ChildLayerPosition
, newBrushName :: !( Maybe Text )
}
deriving stock ( Show, Generic ) deriving stock ( Show, Generic )
deriving anyclass NFData deriving anyclass NFData

View file

@ -15,8 +15,6 @@ import Control.Monad.ST
( RealWorld, ST, runST ) ( RealWorld, ST, runST )
import Data.Foldable import Data.Foldable
( for_ ) ( for_ )
import Data.Functor.Identity
( Identity(..) )
import GHC.Generics import GHC.Generics
( Generic ) ( Generic )
import GHC.TypeLits import GHC.TypeLits
@ -59,8 +57,6 @@ import Math.Linear
( (..) ) ( (..) )
-- MetaBrush -- MetaBrush
import MetaBrush.Assert
( assert )
import MetaBrush.Brush import MetaBrush.Brush
( NamedBrush(..) ) ( NamedBrush(..) )
import MetaBrush.Document import MetaBrush.Document
@ -72,9 +68,6 @@ import MetaBrush.Stroke
import MetaBrush.Unique import MetaBrush.Unique
( Unique, UniqueSupply, freshUnique ) ( Unique, UniqueSupply, freshUnique )
import Unsafe.Coerce
( unsafeCoerce )
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- | A draw anchor, to continue drawing from one end of an existing stroke. -- | A draw anchor, to continue drawing from one end of an existing stroke.
@ -233,7 +226,7 @@ addToAnchor anchor newSpline doc@( Document { documentMetadata, documentContent
updateStroke strokeUnique stroke@( Stroke { strokeSpline = ( oldSpline :: StrokeSpline clo brushParams ) }) updateStroke strokeUnique stroke@( Stroke { strokeSpline = ( oldSpline :: StrokeSpline clo brushParams ) })
| strokeUnique == anchorStroke anchor | strokeUnique == anchorStroke anchor
, SOpen <- ssplineType @clo , SOpen <- ssplineType @clo
, let prevSpline0 = co @RealWorld @s oldSpline , let prevSpline0 = coCache @RealWorld @s oldSpline
= do = do
finalSpline <- finalSpline <-
if anchorIsAtEnd anchor if anchorIsAtEnd anchor
@ -270,14 +263,10 @@ addToAnchor anchor newSpline doc@( Document { documentMetadata, documentContent
return ( 0, prevSpline0 ) return ( 0, prevSpline0 )
newSpline' <- newCaches ( \ i -> i0 - fromIntegral i - 1 ) ( brushParams ( splineStart prevSpline ) ) ( reverseSpline newSpline ) newSpline' <- newCaches ( \ i -> i0 - fromIntegral i - 1 ) ( brushParams ( splineStart prevSpline ) ) ( reverseSpline newSpline )
return $ newSpline' <> prevSpline return $ newSpline' <> prevSpline
return $ UpdateStrokeTo ( stroke { strokeSpline = co @s @RealWorld finalSpline } ) return $ UpdateStrokeTo ( stroke { strokeSpline = coCache @s @RealWorld finalSpline } )
| otherwise | otherwise
= return PreserveStroke = return PreserveStroke
{-# NOINLINE co #-}
co :: forall s t brushParams. Spline Open ( CurveData s ) ( PointData brushParams ) -> Spline Open ( CurveData t ) ( PointData brushParams )
co = unsafeCoerce
newCaches :: ( Int -> Rational ) newCaches :: ( Int -> Rational )
-> brushParams -> brushParams
-> Spline Open () ( PointData () ) -> Spline Open () ( PointData () )

View file

@ -7,7 +7,7 @@ module MetaBrush.Stroke where
import Control.Arrow import Control.Arrow
( (***) ) ( (***) )
import Control.Monad.ST import Control.Monad.ST
( RealWorld ) ( ST, RealWorld, runST )
import Data.Coerce import Data.Coerce
( coerce ) ( coerce )
import Data.Foldable import Data.Foldable
@ -22,6 +22,8 @@ import GHC.Stack
( HasCallStack ) ( HasCallStack )
import GHC.TypeLits import GHC.TypeLits
( Symbol ) ( Symbol )
import Unsafe.Coerce
( unsafeCoerce )
-- acts -- acts
import Data.Act import Data.Act
@ -65,10 +67,10 @@ import qualified Control.Monad.Trans.State.Strict as State
-- brush-strokes -- brush-strokes
import Math.Bezier.Spline import Math.Bezier.Spline
( Spline(..), KnownSplineType ( Spline(..), KnownSplineType
, PointType(..) , PointType(..), bitraverseSpline, bitraverseCurve
) )
import Math.Bezier.Stroke import Math.Bezier.Stroke
( CachedStroke ) ( CachedStroke, newCache )
import Math.Module import Math.Module
( Module ( Module
( origin, (^+^), (^-^), (*^) ) ( origin, (^+^), (^-^), (*^) )
@ -181,6 +183,31 @@ overStrokeSpline
-> Stroke -> Stroke -> Stroke -> Stroke
overStrokeSpline f = coerce ( _strokeSpline @Identity ( coerce . f ) ) overStrokeSpline f = coerce ( _strokeSpline @Identity ( coerce . f ) )
setStrokeBrush
:: ( Maybe ( NamedBrush brushFields ) )
-> Stroke -> Stroke
setStrokeBrush brush
( Stroke { strokeSpline = ( oldStrokeSpline :: StrokeSpline clo pointParams ) } ) =
-- Invalidate all of the cached brush strokes.
let spline' :: ST s ( Spline clo ( CurveData s ) ( PointData pointParams ) )
spline' = bitraverseSpline
( \ _ -> bitraverseCurve invalidateCurve ( const return ) )
return
oldStrokeSpline
in
Stroke
{ strokeSpline = runST $ coCache <$> spline'
, strokeBrush = brush }
where
invalidateCurve :: CurveData RealWorld -> ST s ( CurveData s )
invalidateCurve crv = do
noCache <- newCache
return $ crv { cachedStroke = noCache }
{-# NOINLINE coCache #-}
coCache :: forall s t clo ptData. Spline clo ( CurveData s ) ptData -> Spline clo ( CurveData t ) ptData
coCache = unsafeCoerce
instance Act ( T ( 2 ) ) ( PointData params ) where instance Act ( T ( 2 ) ) ( PointData params ) where
v ( dat@( PointData { pointCoords = p } ) ) = v ( dat@( PointData { pointCoords = p } ) ) =
dat { pointCoords = v p } dat { pointCoords = v p }