packages feed

monomer-hagrid 0.1.0.0 → 0.2.0.0

raw patch · 9 files changed

+984/−444 lines, 9 filesdep +randomdep ~bytestringdep ~containersdep ~data-defaultnew-component:exe:example-basicnew-component:exe:example-big-gridnew-component:exe:example-resizing-cellsPVP ok

version bump matches the API change (PVP)

Dependencies added: random

Dependency ranges changed: bytestring, containers, data-default, data-default-class, ilist, lens, mtl, stm, text, time

API changes (from Hackage documentation)

+ Monomer.Hagrid: estimatedItemHeight :: Double -> HagridCfg s e
+ Monomer.Hagrid: instance GHC.Classes.Eq Monomer.Hagrid.ContentPanePhase
+ Monomer.Hagrid: instance GHC.Classes.Eq a => GHC.Classes.Eq (Monomer.Hagrid.ContentPaneModel a)
+ Monomer.Hagrid: instance GHC.Show.Show Monomer.Hagrid.ContentPanePhase
+ Monomer.Hagrid: instance GHC.Show.Show a => GHC.Show.Show (Monomer.Hagrid.ContentPaneModel a)
+ Monomer.Hagrid: type ItemWithIndex a = (a, Int)
- Monomer.Hagrid: CustomFooterWidget :: (forall s. WidgetModel s => [(a, Int)] -> WidgetNode s e) -> ColumnFooterWidget e a
+ Monomer.Hagrid: CustomFooterWidget :: (forall s. WidgetModel s => Seq (ItemWithIndex a) -> WidgetNode s e) -> ColumnFooterWidget e a
- Monomer.Hagrid: hagrid :: forall a s e. (CompositeModel a, WidgetModel s, WidgetEvent e) => [Column e a] -> [a] -> WidgetNode s e
+ Monomer.Hagrid: hagrid :: forall a s e. (CompositeModel a, WidgetModel s, WidgetEvent e) => [Column e a] -> Seq a -> WidgetNode s e
- Monomer.Hagrid: hagrid_ :: forall a s e. (CompositeModel a, WidgetModel s, WidgetEvent e) => [HagridCfg s e] -> [Column e a] -> [a] -> WidgetNode s e
+ Monomer.Hagrid: hagrid_ :: forall a s e. (CompositeModel a, WidgetModel s, WidgetEvent e) => [HagridCfg s e] -> [Column e a] -> Seq a -> WidgetNode s e
- Monomer.Hagrid: type ScrollToRowCallback a = [(a, Int)] " The items in the grid, in the originally provided order, along with each item's index in the current grid order." -> Maybe Int " The row to scroll to, as an index into the sorted items (e.g. 0 is always the first row in the grid, regardless of the current order). 'Nothing' will cancel the scroll."
+ Monomer.Hagrid: type ScrollToRowCallback a = Seq (ItemWithIndex a) " The items in the grid, in the originally provided order, along with each item's index in the current grid order." -> Maybe Int " The row to scroll to, as an index into the sorted items (e.g. 0 is always the first row in the grid, regardless of the current order). 'Nothing' will cancel the scroll."

Files

ChangeLog.md view
@@ -1,3 +1,11 @@+## 0.2.0.0+### Added+- Speed up large grids by only creating widgets that are visible.+- Use `Seq` rather than lists, for better performance with large grids (breaks API).+- New "resizing-cells" example to show what happens when cells resize.+### Fixed+- The footer now only takes up vertical space if there are some footer widgets defined.+ ## 0.1.0.0  Initial release.
README.md view
@@ -7,7 +7,7 @@ ## Current Status - Somewhat buggy and incomplete. Don't build your business on this. -## Features+## Current Features - Vertical and horizontal scrolling. - Resizable columns. - Column sorting, with a customizable sort key.@@ -16,21 +16,25 @@ - Custom width/height padding for each cell, configured per column. - Scrolling to a particular row when sent a message. - Custom footer widgets on the bottom of each column.+- Efficient implementation that only inflates visible widgets, scaling to at least 10000 rows. -## Goals-- Have complete and helpful documentation.-- Be reasonably configurable and inspectable.-- Be no uglier than standard Monomer widgets.-- Be performant with up to ten thousand rows.-- Have some tests.+## Definite Future Goals+- Better documention.+- Better tests.+- Prettier appearance. -## Todo List-- Improve performance with lots of rows (e.g. 1000).+## Possible Future Goals - Explore providing an API where current sort/column widths/etc are stored in the parent model and updated by hagrid via a lens. This may remove some complexity around merging and the specification of initial sorts/column widths. +## API Documentation++See the generated docs on [Hackage](https://hackage.haskell.org/package/monomer-hagrid).+ ## To build and run examples ```bash-stack run+stack run example-basic             # Demo of the basic functionality.+stack run example-big-grid          # Demo of a grid with over 10000 rows.+stack run example-resizing-cells    # Demo where the cells are constantly changing size. ```  ## Contribution Guide
+ example-basic/Main.hs view
@@ -0,0 +1,216 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell #-}+{-# OPTIONS_GHC -Wno-name-shadowing #-}++module Main (main) where++import Control.Lens (Ixed (ix), makeLensesFor, singular)+import Data.Sequence (Seq ((:|>)))+import qualified Data.Sequence as S+import Data.Text (Text, pack)+import qualified Data.Text as T+import Data.Time (Day, addDays, defaultTimeLocale, formatTime, fromGregorian)+import Monomer+import Monomer.Hagrid (Column (..), ColumnAlign (..), ColumnFooterWidget (..), ColumnSortKey (..), SortDirection (..), hagrid_, initialSort, scrollToRow, showOrdColumn, textColumn, widgetColumn)+import Text.Printf (printf)++data AppModel = AppModel+  { theme :: Theme,+    spiders :: Seq Spider,+    columns :: [AppColumn],+    rowToScrollTo :: Int+  }+  deriving (Eq, Show)++newtype AppColumn = AppColumn+  {enabled :: Bool}+  deriving (Eq, Show)++data AppEvent+  = FeedSpider Int+  | AddSpider+  | NameColumnResized Int+  | NameColumnSorted SortDirection+  | ScrollToOriginalIndex++data Spider = Spider+  { index :: Int,+    species :: Text,+    name :: Text,+    dateOfBirth :: Day,+    weightKilos :: Double+  }+  deriving (Eq, Show)++makeLensesFor [("enabled", "_enabled")] ''AppColumn+makeLensesFor [("columns", "_columns"), ("theme", "_theme"), ("rowToScrollTo", "_rowToScrollTo")] ''AppModel++main :: IO ()+main = startApp model handleEvent buildUI config+  where+    config =+      [ appWindowTitle "Hagrid Basic Example",+        appFontDef "Bold" "./assets/fonts/Cantarell/Cantarell-Bold.ttf",+        appFontDef "Regular" "./assets/fonts/Cantarell/Cantarell-Regular.ttf",+        appDisableAutoScale True,+        appWindowState (MainWindowNormal (1200, 1000))+      ]+    model =+      AppModel+        { theme = darkTheme,+          spiders = spiders,+          columns = AppColumn True <$ gridColumns,+          rowToScrollTo = 0+        }+    spiders = S.fromFunction numSpiders spider+    spider i =+      Spider+        { index = i,+          species = "Acromantula",+          name = T.pack (printf "Son of Aragog %d" (i + 1)),+          dateOfBirth = addDays (fromIntegral i) (fromGregorian 1942 3 1),+          weightKilos = fromIntegral (numSpiders + 2 - i) * 2.3+        }+    numSpiders = 100++buildUI :: UIBuilder AppModel AppEvent+buildUI _wenv model = tree+  where+    tree =+      themeSwitch_ model.theme [themeClearBg] $+        vstack+          [ grid `nodeKey` hagridKey,+            vstack_+              [childSpacing]+              (themeConfigurer : columnConfigurers <> actionButtons)+              `styleBasic` [padding 8]+          ]++    grid =+      hagrid_+        [initialSort 1 SortDescending]+        (mconcat (zipWith column model.columns gridColumns))+        model.spiders++    column (AppColumn enabled) columnDef =+      [columnDef | enabled]++    themeConfigurer =+      hstack_+        [childSpacing]+        [ labeledRadio_ "Dark Theme" darkTheme _theme [textRight],+          labeledRadio_ "Light Theme" lightTheme _theme [textRight]+        ]++    columnConfigurers =+      zipWith columnConfigurer [0 .. length model.columns - 1] gridColumns++    columnConfigurer :: Int -> Column AppEvent Spider -> WidgetNode AppModel AppEvent+    columnConfigurer idx columnDef =+      labeledCheckbox_+        columnDef.name+        (_columns . singular (ix idx) . _enabled)+        [textRight]++    actionButtons =+      [ hstack_+          [childSpacing]+          [ label "Scroll to index of unsorted list",+            numericField _rowToScrollTo,+            button "Go!" ScrollToOriginalIndex+          ],+        hstack_+          [childSpacing]+          [button "Add spider" AddSpider]+      ]++handleEvent :: EventHandler AppModel AppEvent sp ep+handleEvent _wenv _node model = \case+  FeedSpider idx+    | Just spdr <- S.lookup idx model.spiders ->+        [ Producer (const (putStrLn ("Feeding spider " <> T.unpack spdr.name))),+          Model model {spiders = S.update idx spdr {weightKilos = spdr.weightKilos + 1} model.spiders}+        ]+  FeedSpider _ ->+    []+  AddSpider ->+    [Model model {spiders = model.spiders :|> newSpider model}]+  NameColumnResized colWidth ->+    [Producer (const (putStrLn ("Name column was resized: " <> show colWidth)))]+  NameColumnSorted direction ->+    [Producer (const (putStrLn ("Name column was sorted: " <> show direction)))]+  ScrollToOriginalIndex ->+    [scrollToRow (WidgetKey hagridKey) (rowScrollIndex model)]++newSpider :: AppModel -> Spider+newSpider model =+  Spider+    { index = fromIntegral (length model.spiders),+      name = "Extra Spider " <> pack (show (length model.spiders)),+      species = "Spider plant",+      dateOfBirth = fromGregorian 2022 6 26,+      weightKilos = 0.01+    }++rowScrollIndex :: AppModel -> Seq (Spider, Int) -> Maybe Int+rowScrollIndex model items =+  snd <$> S.lookup model.rowToScrollTo items++gridColumns :: [Column AppEvent Spider]+gridColumns = cols+  where+    cols =+      [ (showOrdColumn "Index" (.index))+          { initialWidth = 120,+            align = ColumnAlignRight,+            footerWidget = CustomFooterWidget countFooter+          },+        (textColumn "Name" (.name))+          { initialWidth = 300,+            resizeHandler = Just NameColumnResized,+            sortHandler = Just NameColumnSorted+          },+        (textColumn "Species" (.species))+          { initialWidth = 200+          },+        (textColumn "Date of Birth" (T.pack . formatTime defaultTimeLocale "%Y-%m-%d" . dateOfBirth))+          { initialWidth = 200+          },+        (textColumn "Weight (Kg)" (T.pack . printf "%.2f" . weightKilos))+          { sortKey = SortWith weightKilos,+            initialWidth = 200,+            align = ColumnAlignRight,+            footerWidget = CustomFooterWidget sumWeightFooter+          },+        (widgetColumn "Actions" actionsColumn)+          { initialWidth = 100,+            paddingW = 5,+            paddingH = 5+          }+      ]++    countFooter spiders =+      labelledFooter "Count" (T.pack . show . length $ spiders)++    sumWeightFooter spiders = tree+      where+        tree = labelledFooter "Sum" (T.pack (printf "%.2f" totalWeightKilos))+        totalWeightKilos = sum (weightKilos . fst <$> spiders)++    labelledFooter labelText text =+      hstack+        [ label labelText,+          filler,+          label text+            `styleBasic` [textFont "Bold"]+        ]+        `styleBasic` [padding 10]++    actionsColumn :: Int -> Spider -> WidgetNode s AppEvent+    actionsColumn idx _spdr =+      button "Feed" (FeedSpider idx)++hagridKey :: Text+hagridKey = "SpiderHagrid"
+ example-big-grid/Main.hs view
@@ -0,0 +1,62 @@+{-# LANGUAGE EmptyCase #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE NamedFieldPuns #-}++module Main (main) where++import Data.Function ((&))+import Data.Sequence (Seq)+import qualified Data.Sequence as S+import Data.Text (Text, dropWhile, dropWhileEnd, pack, splitOn, strip)+import Data.Text.IO (readFile)+import Monomer+import Monomer.Hagrid (estimatedItemHeight, hagrid_, initialWidth, textColumn, widgetColumn)+import Prelude hiding (dropWhile, readFile)++newtype AppModel = AppModel+  { paragraphs :: Seq Text+  }+  deriving (Eq, Show)++data AppEvent++main :: IO ()+main = do+  paragraphs <- splitParagraphs <$> readFile "./assets/etc/war-and-peace.txt"+  startApp (model paragraphs) handleEvent buildUI config+  where+    config =+      [ appWindowTitle "Hagrid Big Grid Example",+        appFontDef "Bold" "./assets/fonts/Cantarell/Cantarell-Bold.ttf",+        appFontDef "Regular" "./assets/fonts/Cantarell/Cantarell-Regular.ttf",+        appTheme darkTheme,+        appDisableAutoScale True,+        appWindowState (MainWindowNormal (1200, 1000))+      ]+    model paragraphs =+      AppModel+        { paragraphs+        }++buildUI :: UIBuilder AppModel AppEvent+buildUI _wenv model = tree+  where+    tree =+      hagrid_+        [estimatedItemHeight 100]+        [ (textColumn "Author" (const "Leo Tolstoy")) {initialWidth = 180},+          (textColumn "Title" (const "War and Peace")) {initialWidth = 160},+          widgetColumn "Line Index" (\i _ -> label (pack (show i))),+          (widgetColumn "Line" (\_ para -> label_ para [multiline, ellipsis])) {initialWidth = 760}+        ]+        model.paragraphs++handleEvent :: EventHandler AppModel AppEvent sp ep+handleEvent _wenv _node _model = \case {}++splitParagraphs :: Text -> Seq Text+splitParagraphs s =+  splitOn "\n\n" s+    & S.fromList+    & fmap (dropWhile (== '\n') . dropWhileEnd (== '\n'))+    & S.filter ((/= mempty) . strip)
+ example-resizing-cells/Main.hs view
@@ -0,0 +1,115 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE NumericUnderscores #-}++module Main (main) where++import Control.Concurrent (threadDelay)+import Control.Monad (forM, forever)+import Data.Default.Class (def)+import Data.Sequence (Seq)+import qualified Data.Sequence as S+import Data.Text (Text, pack, words)+import Monomer+import Monomer.Hagrid (estimatedItemHeight, hagrid_, initialWidth, minWidth, showOrdColumn, textColumn, widgetColumn)+import Monomer.Widgets.Single+import System.Random.Stateful (globalStdGen, uniformRM)+import Prelude hiding (words)++newtype AppModel = AppModel+  { cellModels :: Seq CellModel+  }+  deriving (Eq, Show)++data CellModel = CellModel+  { info :: Text,+    dotColor :: Color,+    dotSize :: Double,+    dotSpeed :: Int,+    dotPhase :: Int+  }+  deriving (Eq, Show)++data AppEvent = AppInit | UpdatePhase++main :: IO ()+main = do+  cellModels <- forM (take 1_000 (zip infos colors)) $ \(info, dotColor) -> do+    dotSize <- uniformRM (30, 200) globalStdGen+    dotPhase <- uniformRM (0, 180) globalStdGen+    dotSpeed <- uniformRM (1, 4) globalStdGen+    pure CellModel {info, dotColor, dotSize, dotPhase, dotSpeed}+  startApp (model cellModels) handleEvent buildUI config+  where+    config =+      [ appWindowTitle "Hagrid Resizing Cells Example",+        appFontDef "Bold" "./assets/fonts/Cantarell/Cantarell-Bold.ttf",+        appFontDef "Regular" "./assets/fonts/Cantarell/Cantarell-Regular.ttf",+        appTheme darkTheme,+        appDisableAutoScale True,+        appWindowState (MainWindowNormal (1_200, 1_000)),+        appInitEvent AppInit+      ]+    model cellModels =+      AppModel {cellModels = S.fromList cellModels}+    colors =+      cycle [red, green, blue, yellow]+    infos =+      cycle . words $+        "This demo shows how hagrid behaves when cells are dynamically changing size."++buildUI :: UIBuilder AppModel AppEvent+buildUI _wenv model = tree+  where+    tree =+      hagrid_+        [estimatedItemHeight 60]+        [ widgetColumn "Line Index" (\i _ -> label (pack (show i))),+          (textColumn "Info" (.info)) {initialWidth = 200},+          (showOrdColumn "Phase" (.dotPhase)) {initialWidth = 300},+          (widgetColumn "Dot" dotWidget) {minWidth = 220}+        ]+        model.cellModels++handleEvent :: EventHandler AppModel AppEvent sp ep+handleEvent _wenv _node model = \case+  AppInit ->+    [ Producer $ \emit -> do+        forever $ do+          emit UpdatePhase+          threadDelay (1_000_000 `div` 30)+    ]+  UpdatePhase ->+    [Model model {cellModels = updatePhase <$> model.cellModels}]+    where+      updatePhase cellModel =+        cellModel {dotPhase = cellModel.dotPhase + cellModel.dotSpeed}++dotWidget :: Int -> CellModel -> WidgetNode s AppEvent+dotWidget _idx cellModel = tree+  where+    tree =+      defaultWidgetNode "DotWidget" $+        createSingle+          cellModel+          def+            { singleGetSizeReq = getSizeReq,+              singleMerge = merge,+              singleRender = render+            }++    size = cellModel.dotSize * abs (sin (fromIntegral cellModel.dotPhase / 100))++    merge _wenv node _oldNode oldState = resultReqs node [resizeReq | needResize]+      where+        resizeReq = ResizeWidgets node._wnInfo._wniWidgetId+        needResize = oldState /= cellModel++    getSizeReq _wenv _node =+      (fixedSize size, fixedSize size)++    render wenv node renderer =+      drawEllipse renderer carea {_rW = size, _rH = size} (Just cellModel.dotColor)+      where+        style = currentStyle wenv node+        carea = getContentArea node style
− examples/Main.hs
@@ -1,218 +0,0 @@-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TemplateHaskell #-}-{-# OPTIONS_GHC -Wno-name-shadowing #-}--module Main (main) where--import Control.Lens (Ixed (ix), makeLensesFor, singular)-import Data.List.Index (setAt)-import Data.Text (Text, pack)-import qualified Data.Text as T-import Data.Time (Day, addDays, defaultTimeLocale, formatTime, fromGregorian)-import Monomer-import Monomer.Hagrid (Column (..), ColumnAlign (..), ColumnFooterWidget (..), ColumnSortKey (..), SortDirection (..), hagrid_, initialSort, scrollToRow, showOrdColumn, textColumn, widgetColumn)-import Text.Printf (printf)--data AppModel = AppModel-  { theme :: Theme,-    spiders :: [Spider],-    columns :: [AppColumn],-    rowToScrollTo :: Int-  }-  deriving (Eq, Show)--newtype AppColumn = AppColumn-  {enabled :: Bool}-  deriving (Eq, Show)--data AppEvent-  = FeedSpider Int-  | AddSpider-  | NameColumnResized Int-  | NameColumnSorted SortDirection-  | ScrollToOriginalIndex--data Spider = Spider-  { index :: Integer,-    species :: Text,-    name :: Text,-    dateOfBirth :: Day,-    weightKilos :: Double-  }-  deriving (Eq, Show)--makeLensesFor [("enabled", "_enabled")] ''AppColumn-makeLensesFor [("columns", "_columns"), ("theme", "_theme"), ("rowToScrollTo", "_rowToScrollTo")] ''AppModel--main :: IO ()-main = startApp model handleEvent buildUI config-  where-    config =-      [ appWindowTitle "Hagrid Examples",-        appFontDef "Bold" "./assets/fonts/Cantarell/Cantarell-Bold.ttf",-        appFontDef "Regular" "./assets/fonts/Cantarell/Cantarell-Regular.ttf",-        appDisableAutoScale True,-        appWindowState (MainWindowNormal (1200, 1000))-      ]-    model =-      AppModel-        { theme = darkTheme,-          spiders = spiders,-          columns = AppColumn True <$ gridColumns,-          rowToScrollTo = 0-        }-    spiders = spider <$> [0 .. numSpiders - 1]-    spider i =-      Spider-        { index = i,-          species = "Acromantula",-          name = T.pack (printf "Son of Aragog %d" (i + 1)),-          dateOfBirth = addDays i (fromGregorian 1942 3 1),-          weightKilos = fromIntegral (numSpiders + 2 - i) * 2.3-        }-    numSpiders = 100--buildUI :: UIBuilder AppModel AppEvent-buildUI _wenv model = tree-  where-    tree =-      themeSwitch_ model.theme [themeClearBg] $-        vstack-          [ grid `nodeKey` hagridKey,-            vstack_-              [childSpacing]-              (themeConfigurer : columnConfigurers <> actionButtons)-              `styleBasic` [padding 8]-          ]--    grid =-      hagrid_-        [initialSort 1 SortDescending]-        (mconcat (zipWith column model.columns gridColumns))-        model.spiders--    column (AppColumn enabled) columnDef =-      [columnDef | enabled]--    themeConfigurer =-      hstack_-        [childSpacing]-        [ labeledRadio_ "Dark Theme" darkTheme _theme [textRight],-          labeledRadio_ "Light Theme" lightTheme _theme [textRight]-        ]--    columnConfigurers =-      zipWith columnConfigurer [0 .. length model.columns - 1] gridColumns--    columnConfigurer :: Int -> Column AppEvent Spider -> WidgetNode AppModel AppEvent-    columnConfigurer idx columnDef =-      labeledCheckbox_-        columnDef.name-        (_columns . singular (ix idx) . _enabled)-        [textRight]--    actionButtons =-      [ hstack_-          [childSpacing]-          [ label "Scroll to index of unsorted list",-            numericField _rowToScrollTo,-            button "Go!" ScrollToOriginalIndex-          ],-        hstack_-          [childSpacing]-          [button "Add spider" AddSpider]-      ]--handleEvent :: EventHandler AppModel AppEvent sp ep-handleEvent _wenv _node model = \case-  FeedSpider idx -> evts-    where-      evts =-        [ Producer (const (putStrLn ("Feeding spider " <> T.unpack spdr.name))),-          Model model {spiders = setAt idx spdr {weightKilos = spdr.weightKilos + 1} model.spiders}-        ]-      spdr = model.spiders !! idx-  AddSpider ->-    [Model model {spiders = model.spiders <> [newSpider model]}]-  NameColumnResized colWidth ->-    [Producer (const (putStrLn ("Name column was resized: " <> show colWidth)))]-  NameColumnSorted direction ->-    [Producer (const (putStrLn ("Name column was sorted: " <> show direction)))]-  ScrollToOriginalIndex ->-    [scrollToRow (WidgetKey hagridKey) (rowScrollIndex model)]--newSpider :: AppModel -> Spider-newSpider model =-  Spider-    { index = fromIntegral (length model.spiders),-      name = "Extra Spider " <> pack (show (length model.spiders)),-      species = "Spider plant",-      dateOfBirth = fromGregorian 2022 6 26,-      weightKilos = 0.01-    }--rowScrollIndex :: AppModel -> [(Spider, Int)] -> Maybe Int-rowScrollIndex model items-  | model.rowToScrollTo >= 0 && model.rowToScrollTo < length items =-      Just (snd (items !! model.rowToScrollTo))-  | otherwise =-      Nothing--gridColumns :: [Column AppEvent Spider]-gridColumns = cols-  where-    cols =-      [ (showOrdColumn "Index" (.index))-          { initialWidth = 120,-            align = ColumnAlignRight,-            footerWidget = CustomFooterWidget countFooter-          },-        (textColumn "Name" (.name))-          { initialWidth = 300,-            resizeHandler = Just NameColumnResized,-            sortHandler = Just NameColumnSorted-          },-        (textColumn "Species" (.species))-          { initialWidth = 200-          },-        (textColumn "Date of Birth" (T.pack . formatTime defaultTimeLocale "%Y-%m-%d" . dateOfBirth))-          { initialWidth = 200-          },-        (textColumn "Weight (Kg)" (T.pack . printf "%.2f" . weightKilos))-          { sortKey = SortWith weightKilos,-            initialWidth = 200,-            align = ColumnAlignRight,-            footerWidget = CustomFooterWidget sumWeightFooter-          },-        (widgetColumn "Actions" actionsColumn)-          { initialWidth = 100,-            paddingW = 5,-            paddingH = 5-          }-      ]--    countFooter spiders =-      labelledFooter "Count" (T.pack . show . length $ spiders)--    sumWeightFooter spiders = tree-      where-        tree = labelledFooter "Sum" (T.pack (printf "%.2f" totalWeightKilos))-        totalWeightKilos = sum (weightKilos . fst <$> spiders)--    labelledFooter labelText text =-      hstack-        [ label labelText,-          filler,-          label text-            `styleBasic` [textFont "Bold"]-        ]-        `styleBasic` [padding 10]--    actionsColumn :: Int -> Spider -> WidgetNode s AppEvent-    actionsColumn idx _spdr =-      button "Feed" (FeedSpider idx)--hagridKey :: Text-hagridKey = "SpiderHagrid"
monomer-hagrid.cabal view
@@ -1,6 +1,6 @@ cabal-version:      2.0 name:               monomer-hagrid-version:            0.1.0.0+version:            0.2.0.0 license:            MIT license-file:       LICENSE maintainer:         garethdanielsmith@gmail.com@@ -36,69 +36,122 @@      build-depends:         base >=4.7 && <5,-        containers >=0.6.5.1 && <0.7,-        data-default-class >=0.1.2.0 && <0.2,-        ilist >=0.4.0.1 && <0.5,-        lens >=5.1.1 && <5.2,+        containers <0.7,+        data-default-class <0.2,+        ilist <0.5,+        lens <5.2,         monomer >=1.5.0.0 && <1.6,-        text >=1.2.5.0 && <1.3+        text <1.3 -executable examples+executable example-basic     main-is:            Main.hs-    hs-source-dirs:     examples+    hs-source-dirs:     example-basic     other-modules:      Paths_monomer_hagrid+    autogen-modules:    Paths_monomer_hagrid     default-language:   Haskell2010     default-extensions:         DisambiguateRecordFields DuplicateRecordFields FlexibleContexts         OverloadedRecordDot OverloadedStrings      ghc-options:-        -threaded -rtsopts -with-rtsopts=-N -Wall -Wcompat-        -Wincomplete-record-updates -Wincomplete-uni-patterns-        -Wredundant-constraints+        -Wall -Wcompat -Wincomplete-record-updates+        -Wincomplete-uni-patterns -Wredundant-constraints -threaded+        -rtsopts -with-rtsopts=-N      build-depends:         base >=4.7 && <5,-        containers >=0.6.5.1 && <0.7,-        data-default-class >=0.1.2.0 && <0.2,-        ilist >=0.4.0.1 && <0.5,-        lens >=5.1.1 && <5.2,+        containers <0.7,+        data-default-class <0.2,+        ilist <0.5,+        lens <5.2,         monomer >=1.5.0.0 && <1.6,-        monomer-hagrid -any,-        text >=1.2.5.0 && <1.3,-        time >=1.11.1.1 && <1.12+        monomer-hagrid,+        text <1.3,+        time <1.12 +executable example-big-grid+    main-is:            Main.hs+    hs-source-dirs:     example-big-grid+    other-modules:      Paths_monomer_hagrid+    autogen-modules:    Paths_monomer_hagrid+    default-language:   Haskell2010+    default-extensions:+        DisambiguateRecordFields DuplicateRecordFields FlexibleContexts+        OverloadedRecordDot OverloadedStrings++    ghc-options:+        -Wall -Wcompat -Wincomplete-record-updates+        -Wincomplete-uni-patterns -Wredundant-constraints -threaded+        -rtsopts -with-rtsopts=-N++    build-depends:+        base >=4.7 && <5,+        containers <0.7,+        data-default-class <0.2,+        ilist <0.5,+        lens <5.2,+        monomer >=1.5.0.0 && <1.6,+        monomer-hagrid,+        text <1.3++executable example-resizing-cells+    main-is:            Main.hs+    hs-source-dirs:     example-resizing-cells+    other-modules:      Paths_monomer_hagrid+    autogen-modules:    Paths_monomer_hagrid+    default-language:   Haskell2010+    default-extensions:+        DisambiguateRecordFields DuplicateRecordFields FlexibleContexts+        OverloadedRecordDot OverloadedStrings++    ghc-options:+        -Wall -Wcompat -Wincomplete-record-updates+        -Wincomplete-uni-patterns -Wredundant-constraints -threaded+        -rtsopts -with-rtsopts=-N++    build-depends:+        base >=4.7 && <5,+        containers <0.7,+        data-default-class <0.2,+        ilist <0.5,+        lens <5.2,+        monomer >=1.5.0.0 && <1.6,+        monomer-hagrid,+        random <1.3,+        text <1.3+ test-suite spec     type:               exitcode-stdio-1.0     main-is:            Spec.hs-    build-tool-depends: hspec-discover:hspec-discover ==2.*+    build-tool-depends: hspec-discover:hspec-discover >=2 && <3     hs-source-dirs:     test     other-modules:         Monomer.HagridSpec         Monomer.TestUtil         Paths_monomer_hagrid +    autogen-modules:    Paths_monomer_hagrid     default-language:   Haskell2010     default-extensions:         DisambiguateRecordFields DuplicateRecordFields FlexibleContexts         OverloadedRecordDot OverloadedStrings      ghc-options:-        -threaded -rtsopts -with-rtsopts=-N -Wall -Wcompat-        -Wincomplete-record-updates -Wincomplete-uni-patterns-        -Wredundant-constraints+        -Wall -Wcompat -Wincomplete-record-updates+        -Wincomplete-uni-patterns -Wredundant-constraints -threaded+        -rtsopts -with-rtsopts=-N      build-depends:         base >=4.7 && <5,-        bytestring >=0.11.3.1 && <0.12,-        containers >=0.6.5.1 && <0.7,-        data-default >=0.7.1.1 && <0.8,-        data-default-class >=0.1.2.0 && <0.2,-        hspec ==2.*,-        ilist >=0.4.0.1 && <0.5,-        lens >=5.1.1 && <5.2,+        bytestring <0.12,+        containers <0.7,+        data-default <0.8,+        data-default-class <0.2,+        hspec >=2 && <3,+        ilist <0.5,+        lens <5.2,         monomer >=1.5.0.0 && <1.6,-        monomer-hagrid -any,-        mtl >=2.2.2 && <2.3,-        stm >=2.5.0.2 && <2.6,-        text >=1.2.5.0 && <1.3+        monomer-hagrid,+        mtl <2.3,+        stm <2.6,+        text <1.3
src/Monomer/Hagrid.hs view
@@ -3,7 +3,6 @@ {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TupleSections #-} {-# OPTIONS_GHC -Wno-name-shadowing #-}  -- | A datagrid widget for the Monomer UI library.@@ -16,9 +15,11 @@     ColumnFooterWidget (..),     ColumnSortKey (..),     SortDirection (..),+    ItemWithIndex,     ScrollToRowCallback,      -- * Configuration options+    estimatedItemHeight,     initialSort,      -- * Hagrid constructors@@ -36,16 +37,15 @@ where  import Control.Applicative ((<|>))-import Control.Lens ((.~), (<>~), (^.))+import Control.Lens (ix, (.~), (<>~), (^.), (^?!)) import Control.Lens.Combinators (non) import Control.Lens.Lens ((&)) import Control.Monad as X (forM_) import Data.Data (Typeable)-import Data.Default.Class as X (Default, def)+import Data.Default.Class (Default, def) import Data.Foldable (foldl')-import qualified Data.List as List import Data.List.Index (indexed, izipWith, modifyAt)-import Data.Maybe (fromJust, maybeToList)+import Data.Maybe (catMaybes, fromJust, isNothing, maybeToList) import Data.Maybe as X (fromMaybe) import Data.Ord (Down (Down)) import Data.Sequence (Seq ((:<|), (:|>)))@@ -60,18 +60,38 @@  -- | Configuration options for Hagrid widgets. data HagridCfg s e = HagridCfg-  { cfgInitialSort :: Maybe (Int, SortDirection)+  { cfgEstimatedItemHeight :: Maybe Double,+    cfgInitialSort :: Maybe (Int, SortDirection)   }  instance Default (HagridCfg s e) where-  def = HagridCfg {cfgInitialSort = Nothing}+  def =+    HagridCfg+      { cfgEstimatedItemHeight = Nothing,+        cfgInitialSort = Nothing+      }  instance Semigroup (HagridCfg s e) where-  c1 <> c2 = HagridCfg {cfgInitialSort = c2.cfgInitialSort <|> c1.cfgInitialSort}+  c1 <> c2 =+    HagridCfg+      { cfgEstimatedItemHeight = c2.cfgEstimatedItemHeight <|> c1.cfgEstimatedItemHeight,+        cfgInitialSort = c2.cfgInitialSort <|> c1.cfgInitialSort+      }  instance Monoid (HagridCfg s e) where   mempty = def +-- | Configures the estimated item height. This should be the average row height you expect+-- in your grid (including padding). This is used to show scrollbar size and position when+-- there are lots of rows and so not all the rows have been "inflated" into widgets. More+-- accurate values will improve performance and scrollbar position accuracy.+--+-- The default value is 40, which is roughly the height of a single line of text with the default+-- column padding (10).+estimatedItemHeight :: Double -> HagridCfg s e+estimatedItemHeight h =+  def {cfgEstimatedItemHeight = Just h}+ -- | Configures the initial sort column and direction. initialSort ::   -- | The initial sort column (zero-indexed, out of bounds values will have no effect).@@ -80,7 +100,7 @@   SortDirection ->   HagridCfg s e initialSort column direction =-  HagridCfg {cfgInitialSort = Just (column, direction)}+  def {cfgInitialSort = Just (column, direction)}  -- | A column definition. data Column e a = Column@@ -99,9 +119,9 @@     initialWidth :: Int,     -- | The minimum allowed width of the column, in pixels.     minWidth :: Int,-    -- | The padding to the left and right of the widget in each cell of the column, in pixels.+    -- | The padding to the left and right of the widget in each cell of the column, in pixels (the default is 10).     paddingW :: Double,-    -- | The padding above and below the widget in each cell in the column, in pixels.+    -- | The padding above and below the widget in each cell in the column, in pixels (the default is 10).     paddingH :: Double,     -- | An optional event to emit when a user has finished resizing the column. The function receives the new width in pixels.     resizeHandler :: Maybe (Int -> e),@@ -124,7 +144,7 @@     NoFooterWidget   | -- | Create a footer widget. The function receives the items in their current sort     -- order, and also along with each item it's original (unsorted) index.-    CustomFooterWidget (forall s. WidgetModel s => [(a, Int)] -> WidgetNode s e)+    CustomFooterWidget (forall s. WidgetModel s => Seq (ItemWithIndex a) -> WidgetNode s e)  -- | How to align the widget within each cell of a column. data ColumnAlign@@ -145,11 +165,14 @@   | SortDescending   deriving (Eq, Show) +-- | A item in the grid, with its row index.+type ItemWithIndex a = (a, Int)+ -- | Picks an item to scroll to, based on the sorted or original grid contents. type ScrollToRowCallback a =   -- | The items in the grid, in the originally provided order, along with each item's index   -- in the current grid order.-  [(a, Int)] ->+  Seq (ItemWithIndex a) ->   -- | The row to scroll to, as an index into the sorted items (e.g. 0 is always the first row   -- in the grid, regardless of the current order). 'Nothing' will cancel the scroll.   Maybe Int@@ -160,12 +183,14 @@   | ResizeColumn Int Int   | ResizeColumnFinished Int   | forall a. Typeable a => ScrollToRow (ScrollToRowCallback a)+  | ScrollToRect Rect   | ParentEvent ep  data HagridModel a = HagridModel-  { sortedItems :: [(a, Int)], -- each item, plus its index in the original (unsorted) list+  { sortedItems :: Seq (ItemWithIndex a),     columns :: [ModelColumn],-    sortColumn :: Maybe (Int, SortDirection)+    sortColumn :: Maybe (Int, SortDirection),+    mdlEstimatedItemHeight :: Double   }   deriving (Eq, Show) @@ -177,12 +202,12 @@  -- | The state of the header or footer, which have a scroll offset because they -- scroll horizontally along with the content pane.-data OffsetXState = OffsetXState+newtype OffsetXState = OffsetXState   { offsetX :: Double   }   deriving (Eq, Show) -data OffsetXEvent = SetOffsetX Double+newtype OffsetXEvent = SetOffsetX Double  data HeaderDragHandleState = HeaderDragHandleState   { dragStartMouseX :: Double,@@ -190,9 +215,36 @@   }   deriving (Eq, Show) -data ContentPaneMessage a-  = ContentPaneScrollToRow (ScrollToRowCallback a)+data ContentPaneModel a = ContentPaneModel+  { -- | The width of each column.+    columnWidths :: [Int],+    -- | The visible area (relative to the content pane).+    visibleArea :: Rect,+    -- | The index in items of the special row that we try and keep in the same position+    -- (relative to the viewport) when items get resized.+    fixedRowIndex :: Int,+    -- | The Y position of the fixed row within the viewport. When items get resized, we try+    -- and keep this value fixed: it should only change when the user scrolls the viewport itself.+    fixedRowViewportOffset :: Double,+    -- | How many items there are before the inflated items.+    itemsBeforeInflated :: Int,+    -- | The items that have been inflated into actual widgets.+    inflatedItems :: Seq (ItemWithIndex a),+    phase :: ContentPanePhase+  }+  deriving (Eq, Show) +data ContentPanePhase+  = ContentPaneIdle+  | ContentPaneReinflating+  deriving (Eq, Show)++data ContentPaneEvent ep+  = SetVisibleArea {visibleArea :: Rect}+  | InnerResizeComplete+  | ContentPaneParentEvent ep+  | forall a. Typeable a => ContentPaneScrollToRow (ScrollToRowCallback a)+ -- | Creates a hagrid widget, using the default configuration. hagrid ::   forall a s e.@@ -200,7 +252,7 @@   -- | The definitions for each column in the grid.   [Column e a] ->   -- | The items for each row in the grid.-  [a] ->+  Seq a ->   WidgetNode s e hagrid = hagrid_ def @@ -212,7 +264,7 @@   -- | The definitions for each column in the grid.   [Column e a] ->   -- | The items for each row in the grid.-  [a] ->+  Seq a ->   WidgetNode s e hagrid_ cfg columnDefs items = widget   where@@ -237,16 +289,21 @@             ]         contentScroll =           scroll_ [onChange ContentScrollChange] $-            contentPane columnDefs model `nodeKey` contentPaneKey+            contentPaneOuter columnDefs model `nodeKey` contentPaneOuterKey      handleEvent :: EventHandler (HagridModel a) (HagridEvent e) sp e     handleEvent wenv _node model = \case-      ScrollToRow row ->-        [Message (WidgetKey contentPaneKey) (ContentPaneScrollToRow row)]-      ContentScrollChange ScrollStatus {scrollDeltaX} ->+      ScrollToRow callback ->+        [Message (WidgetKey contentPaneOuterKey) (ContentPaneScrollToRow callback :: ContentPaneEvent e)]+      ScrollToRect rect ->+        [Message (WidgetKey contentScrollKey) (ScrollTo rect)]+      ContentScrollChange ScrollStatus {scrollDeltaX, scrollDeltaY, scrollVpSize} ->         [ Message (WidgetKey headerPaneKey) (SetOffsetX scrollDeltaX),+          Message (WidgetKey contentPaneOuterKey) (SetVisibleArea {visibleArea} :: ContentPaneEvent e),           Message (WidgetKey footerPaneKey) (SetOffsetX scrollDeltaX)         ]+        where+          visibleArea = Rect (-scrollDeltaX) (-scrollDeltaY) scrollVpSize._sW scrollVpSize._sH       OrderByColumn colIndex -> result         where           Column {sortHandler, sortKey} = columnDefs !! colIndex@@ -254,7 +311,7 @@             | Just (c, dir) <- model.sortColumn,               c == colIndex =                 let sortColumn = Just (colIndex, flipSortDirection dir)-                    sortedItems = reverse model.sortedItems+                    sortedItems = S.reverse model.sortedItems                  in (sortColumn, sortedItems)             | otherwise =                 let sortColumn = Just (colIndex, SortAscending)@@ -268,8 +325,7 @@       ResizeColumn colIndex newWidth ->         [ Model (model {columns = modifyAt colIndex (\c -> c {currentWidth = newWidth}) model.columns}),           Request (ResizeWidgets headerPaneId),-          Request (ResizeWidgets footerPaneId),-          Request (ResizeWidgets contentPaneId)+          Request (ResizeWidgets footerPaneId)         ]       ResizeColumnFinished colIndex -> result         where@@ -282,7 +338,6 @@       where         headerPaneId = fromJust (widgetIdFromKey wenv (WidgetKey headerPaneKey))         footerPaneId = fromJust (widgetIdFromKey wenv (WidgetKey footerPaneKey))-        contentPaneId = fromJust (widgetIdFromKey wenv (WidgetKey contentPaneKey))      -- If the column names have not changed then preseve the column widths and sort     -- order too, otherwise unrelated model changes will reset the column widths/sort.@@ -387,8 +442,12 @@           where             Rect l t _w h = viewport             widgetWidths = do-              w <- currentWidth <$> model.columns-              [w - dragHandleWidth, dragHandleWidth]+              (i, w) <- indexed (currentWidth <$> model.columns)+              -- center the drag handle inbetween the columns+              let buttonW+                    | i == 0 = w - (dragHandleWidth `div` 2)+                    | otherwise = w - dragHandleWidth+              [buttonW, dragHandleWidth]             (assignedAreas, _) = foldl' assignArea (mempty, l) widgetWidths             assignArea (areas, colX) columnWidth =               (areas :|> Rect colX t (fromIntegral columnWidth) h, colX + fromIntegral columnWidth)@@ -399,16 +458,19 @@         renderSortIndicator wenv node renderer (sortCol, sortDirection) = do           drawSortIndicator renderer indRect (Just (accentColor wenv)) sortDirection           where+            Rect l t w h = node ^?! L.children . ix (sortCol * 2) . L.info . L.viewport+             style = wenv ^. L.theme . L.basic . L.btnStyle-            Rect l t _w h = node ^. L.info . L.viewport             size = style ^. L.text . non def . L.fontSize . non def-            colOffset = fromIntegral (sum (take (sortCol + 1) (currentWidth <$> model.columns)) - dragHandleWidth)-            indW = unFontSize size * 2 / 3-            pad = indW / 3++            -- put triangle corners at integer positions because it looks nicer+            indW = ceilingDouble (unFontSize size * 2 / 3)+            pad = ceilingDouble (unFontSize size * 2 / 9)+             indT = case sortDirection of               SortAscending -> t + h - pad - indW               SortDescending -> t + pad-            indL = l + state.offsetX + colOffset - indW - pad+            indL = l + w + state.offsetX - indW - pad             indRect = Rect indL indT indW indW  headerButton :: WidgetEvent ep => Int -> Column ep a -> WidgetNode s (HagridEvent ep)@@ -416,7 +478,12 @@   button_ columnDef.name (OrderByColumn colIndex) [ellipsis]     `styleBasic` [radius 0] -footerPane :: forall s ep a. (CompositeModel a, CompositeModel s, Typeable ep) => [Column ep a] -> HagridModel a -> WidgetNode (HagridModel s) (HagridEvent ep)+footerPane ::+  forall s ep a.+  (CompositeModel a, CompositeModel s, Typeable ep) =>+  [Column ep a] ->+  HagridModel a ->+  WidgetNode (HagridModel s) (HagridEvent ep) footerPane columnDefs model = makeNode (OffsetXState 0)   where     makeNode :: OffsetXState -> WidgetNode (HagridModel s) (HagridEvent ep)@@ -424,12 +491,10 @@       where         node =           defaultWidgetNode "Hagrid.FooterPane" (makeWidget state)-            & L.children .~ childWidgets+            & L.children .~ S.fromList (catMaybes childWidgets) -    childWidgets :: Seq (WidgetNode (HagridModel s) (HagridEvent ep))-    childWidgets =-      S.fromList $-        footerWidgetNode model.sortedItems . footerWidget <$> columnDefs+    childWidgets :: [Maybe (WidgetNode (HagridModel s) (HagridEvent ep))]+    childWidgets = footerWidgetNode model.sortedItems . footerWidget <$> columnDefs      makeWidget :: OffsetXState -> Widget (HagridModel s) (HagridEvent ep)     makeWidget state = container@@ -473,14 +538,18 @@         getSizeReq _wenv _node children = (w, h)           where             w = fixedSize (sum (fromIntegral . currentWidth <$> model.columns) + hScrollFudgeFactor)-            h = foldl' sizeReqMergeMax (fixedSize 0) ((_wniSizeReqH . _wnInfo) <$> children)+            h = foldl' sizeReqMergeMax (fixedSize 0) (_wniSizeReqH . _wnInfo <$> children)          resize _wenv node viewport _children = (resultNode node, assignedAreas)           where             Rect l t _w h = viewport-            (assignedAreas, _) = foldl' assignArea (mempty, l) model.columns-            assignArea (areas, colX) ModelColumn {currentWidth} =-              (areas :|> Rect colX t (fromIntegral currentWidth) h, colX + fromIntegral currentWidth)+            (assignedAreas, _) = foldl' assignArea (mempty, l) (zip childWidgets model.columns)+            assignArea (areas, colX) (childWidget, ModelColumn {currentWidth}) = (newAreas, newColX)+              where+                newAreas+                  | isNothing childWidget = areas+                  | otherwise = areas :|> Rect colX t (fromIntegral currentWidth) h+                newColX = colX + fromIntegral currentWidth  headerDragHandle :: WidgetEvent ep => Int -> Column ep a -> ModelColumn -> WidgetNode s (HagridEvent ep) headerDragHandle colIndex columnDef column = tree@@ -545,162 +614,342 @@ hScrollFudgeFactor :: Double hScrollFudgeFactor = 100 -contentPane ::+-- | Composite wrapper to allow creating/removing child widgets during resize.+contentPaneOuter ::   forall a ep.   (CompositeModel a, WidgetEvent ep) =>   [Column ep a] ->   HagridModel a ->   WidgetNode (HagridModel a) (HagridEvent ep)-contentPane columnDefs model = node+contentPaneOuter columnDefs model =+  compositeD_+    "Hagrid.ContentPaneOuter"+    (WidgetValue initialModel)+    buildUI+    handleEvent+    [compositeMergeModel mergeModel]   where-    node =-      defaultWidgetNode "Hagrid.ContentPane" contentPaneContainer-        & L.children .~ S.fromList (mconcat childWidgetRows)+    initialModel =+      ContentPaneModel+        { columnWidths = currentWidth <$> model.columns,+          visibleArea = Rect 0 0 0 0,+          fixedRowIndex = 0,+          fixedRowViewportOffset = 0,+          itemsBeforeInflated = 0,+          inflatedItems = mempty,+          phase = ContentPaneIdle+        } -    childWidgetRows =-      [ [cellWidget idx item widget | Column {widget} <- columnDefs]-        | (item, idx) <- model.sortedItems-      ]+    mergeModel :: MergeModelHandler (ContentPaneModel a) (ContentPaneEvent e) s+    mergeModel _wenv _parentModel oldModel newModel =+      oldModel {columnWidths, fixedRowIndex, itemsBeforeInflated, inflatedItems}+      where+        columnWidths = newModel.columnWidths+        fixedRowIndex = min (max 0 (length model.sortedItems - 1)) oldModel.fixedRowIndex+        itemsBeforeInflated = min (length model.sortedItems) oldModel.itemsBeforeInflated+        inflatedItems = takeAt itemsBeforeInflated (length oldModel.inflatedItems) model.sortedItems -    nCols = length columnDefs-    columnDefsSeq = S.fromList columnDefs+    buildUI _wenv cpModel =+      contentPaneInner (S.fromList columnDefs) model cpModel `nodeKey` contentPaneInnerKey +    handleEvent wenv node cpModel = \case+      SetVisibleArea visibleArea -> result+        where+          result = case cpModel.phase of+            ContentPaneIdle+              | visibleAreaMoved && (startItemsMissing || endItemsMissing) ->+                  let newModel =+                        cpModel+                          { visibleArea,+                            fixedRowIndex,+                            fixedRowViewportOffset,+                            itemsBeforeInflated = fixedRowIndex,+                            inflatedItems = mempty,+                            phase = ContentPaneReinflating+                          }+                   in [Model newModel, resizeInnerRequest wenv]+              | visibleAreaMoved ->+                  let newModel = cpModel {visibleArea, fixedRowIndex, fixedRowViewportOffset}+                   in [Model newModel]+              | otherwise -> []+            ContentPaneReinflating+              | visibleAreaMoved ->+                  let newModel = (cpModel :: ContentPaneModel a) {visibleArea}+                   in [Model newModel]+              | otherwise -> []++          (fixedRowIndex, fixedRowViewportOffset) = fixedRow minVisibleY rowHeights model cpModel+          (rowsStartY, rowHeights, rowsEndY) = rowPositions node++          visibleAreaMoved = not (roundedRectEq visibleArea cpModel.visibleArea)+          minVisibleY = visibleArea._rY+          maxVisibleY = visibleArea._rY + visibleArea._rH++          startItemsMissing = cpModel.itemsBeforeInflated > 0 && minVisibleY < rowsStartY+          endItemsMissing = itemsAfterInflated model cpModel > 0 && maxVisibleY > rowsEndY+      ContentPaneScrollToRow callback -> result+        where+          result+            | Just typedCb <- cast callback,+              Just row <- typedCb indexedItems =+                -- set the fixed index to the target row and let the viewport position be sorted out by the+                -- adjustment that follows the addition and resizing of the rows around the target row.+                let newModel =+                      cpModel+                        { fixedRowIndex = row,+                          fixedRowViewportOffset = 0,+                          itemsBeforeInflated = row,+                          inflatedItems = mempty,+                          phase = ContentPaneReinflating+                        }+                 in [Model newModel, resizeInnerRequest wenv]+            | otherwise =+                []++          indexedItems =+            model.sortedItems+              & S.mapWithIndex (,)+              & S.sortOn (snd . snd)+              & fmap (\(sortedIndex, (item, _originalIndex)) -> (item, sortedIndex))+      InnerResizeComplete -> result+        where+          (rowsStartY, rowHeights, rowsEndY) = rowPositions node++          fixedRowY = rowsStartY + sum (S.take (cpModel.fixedRowIndex - cpModel.itemsBeforeInflated) rowHeights)++          result+            | itemsToPrepend > 0 || itemsToAppend > 0 =+                let inflatedItems =+                      takeAt+                        (cpModel.itemsBeforeInflated - itemsToPrepend)+                        (itemsToPrepend + length cpModel.inflatedItems + itemsToAppend)+                        model.sortedItems+                    itemsBeforeInflated = cpModel.itemsBeforeInflated - itemsToPrepend+                 in [Model cpModel {inflatedItems, itemsBeforeInflated}, resizeInnerRequest wenv]+            | otherwise =+                -- Once we have finished adding items then, if the added items are not the same size as estimated,+                -- the row we want to scroll to might no longer be at the correct position in the viewport, so we+                -- need to adjust the scroll position to position it correctly.+                let adjustScrollEvt = [Report (ScrollToRect adjustScrollRect) | needAdjustScroll]+                    adjustScrollRect = Rect (vp._rX + cpModel.visibleArea._rX) (vp._rY + fixedRowY - cpModel.fixedRowViewportOffset) visibleWidth visibleHeight+                    needAdjustScroll = abs ((fixedRowY - cpModel.visibleArea._rY) - cpModel.fixedRowViewportOffset) >= 1+                 in [Model cpModel {phase = ContentPaneIdle}] <> adjustScrollEvt++          itemsToPrepend = itemsToAdd (fixedRowY - rowsStartY) 1 cpModel.itemsBeforeInflated+          itemsToAppend = itemsToAdd (rowsEndY - fixedRowY) 2 (itemsAfterInflated model cpModel)++          itemsToAdd existingItemsHeight f availableItems+            | not (null model.sortedItems) && existingItemsHeight < visibleHeight * f =+                let n = ceiling ((visibleHeight * f - existingItemsHeight) / model.mdlEstimatedItemHeight)+                 in min (max 8 (min 64 n)) availableItems+            | otherwise = 0++          vp = node ^. L.info . L.viewport+          visibleWidth = cpModel.visibleArea._rW+          visibleHeight = cpModel.visibleArea._rH+      ContentPaneParentEvent ep ->+        [Report (ParentEvent ep)]++contentPaneInner ::+  forall a ep.+  (CompositeModel a, WidgetEvent ep) =>+  Seq (Column ep a) ->+  HagridModel a ->+  ContentPaneModel a ->+  WidgetNode (ContentPaneModel a) (ContentPaneEvent ep)+contentPaneInner columnDefs model cpModel = node+  where+    node =+      defaultWidgetNode "Hagrid.ContentPaneInner" contentPaneContainer+        & L.children .~ rowWidgets+     contentPaneContainer =       createContainer-        model+        cpModel         def+          { containerMerge = merge,+            containerGetSizeReq = getSizeReq,+            containerResize = resize+          }++    rowWidgets = contentPaneRow columnDefs cpModel <$> cpModel.inflatedItems++    merge _wenv newNode _oldNode oldState = resultReqs newNode reqs+      where+        reqs = [ResizeWidgets (newNode ^. L.info . L.widgetId) | needResize]+        needResize = oldState.columnWidths /= cpModel.columnWidths++    getSizeReq _wenv _node children = (w, h)+      where+        w = fixedSize (fromIntegral (sum cpModel.columnWidths))+        h = fixedSize (uninflatedHeights + inflatedHeights)++        uninflatedHeights = fromIntegral uninflatedItems * model.mdlEstimatedItemHeight+        uninflatedItems = cpModel.itemsBeforeInflated + itemsAfterInflated model cpModel++        inflatedHeights = sum (_szrFixed . _wniSizeReqH . _wnInfo <$> children)++    resize wenv node viewport children = (resultEvts node [InnerResizeComplete], rowAreas)+      where+        style = currentStyle wenv node+        innerVp = fromMaybe def (removeOuterBounds style viewport)++        startX = innerVp._rX+        startY = innerVp._rY + fromIntegral cpModel.itemsBeforeInflated * model.mdlEstimatedItemHeight++        sumColumnWidths = fromIntegral (sum cpModel.columnWidths)++        rowAreas = snd (foldl' foldRowAreas (startY, mempty) children)+        foldRowAreas (y, areas) child =+          (y + h, areas :|> Rect startX y sumColumnWidths h)+          where+            h = child ^. L.info . L.sizeReqH . L.fixed++contentPaneRow ::+  forall a ep.+  (CompositeModel a, WidgetEvent ep) =>+  Seq (Column ep a) ->+  ContentPaneModel a ->+  (a, Int) ->+  WidgetNode (ContentPaneModel a) (ContentPaneEvent ep)+contentPaneRow columnDefs cpModel (item, rowIdx) = tree+  where+    tree =+      defaultWidgetNode "Hagrid.Row" widget+        & L.children .~ cellWidgets++    widget =+      createContainer+        (cpModel.columnWidths, item, rowIdx)+        def           { containerGetSizeReq = getSizeReq,             containerResize = resize,-            containerRender = render,-            containerHandleEvent = handleEvent,-            containerHandleMessage = handleMessage+            containerRender = render           } +    cellWidgets = do+      Column {widget} <- columnDefs+      pure (cellWidget rowIdx item widget)+     getSizeReq _wenv _node children = (w, h)       where-        w = fixedSize (sum (fromIntegral . currentWidth <$> model.columns))-        h = fixedSize (sum (toRowHeights children columnDefsSeq))+        w = fixedSize (fromIntegral (sum cpModel.columnWidths))+        h = fixedSize (toRowHeight columnDefs children) -    resize wenv node viewport children = (resultNode node, assignedAreas)+    resize wenv node viewport children = (resultNode node, cellAreas)       where         style = currentStyle wenv node-        Rect l t _w _h = fromMaybe def (removeOuterBounds style viewport)+        innerVp = fromMaybe def (removeOuterBounds style viewport) -        colXs = sizesToPositions (S.fromList (fromIntegral . currentWidth <$> model.columns))-        rowYs = sizesToPositions (toRowHeights children columnDefsSeq)+        startX = innerVp._rX+        startY = innerVp._rY -        assignedAreas = do-          (rowN, row) <--            S.mapWithIndex (\i -> (i,)) (S.chunksOf nCols children)-          (colN, columnDef, widget) <--            S.mapWithIndex (\i (cd, w) -> (i, cd, w)) (S.zip columnDefsSeq row)-          pure (assignArea colN columnDef rowN widget)+        columnWidths = fromIntegral <$> S.fromList cpModel.columnWidths -        assignArea col Column {paddingW, paddingH, align} row widget = Rect chX chY chW chH+        cellAreas = snd (foldl' foldCellAreas (startX, mempty) (S.zip3 columnWidths columnDefs children))+        foldCellAreas (x, areas) (colW, Column {paddingW, paddingH, align}, widget) =+          (x + colW, areas :|> Rect chX cellY chW cellH)           where-            (chX, chW)-              | widgetReqW >= cellW = (cellX, cellW)-              | align == ColumnAlignLeft = (cellX, widgetReqW)-              | otherwise = (cellX + cellW - widgetReqW, widgetReqW)-            (chY, chH) =-              (cellY, cellH)+            (chX, chW) = case align of+              ColumnAlignLeft -> (cellX, cellW)+              ColumnAlignRight -> (cellX + cellW - widgetW, widgetW) -            cellX = l + S.index colXs col + paddingW-            cellY = t + S.index rowYs row + paddingH-            cellW = S.index colXs (col + 1) - S.index colXs col - paddingW * 2-            cellH = S.index rowYs (row + 1) - S.index rowYs row - paddingH * 2+            cellX = x + paddingW+            cellY = startY + paddingH+            cellW = colW - paddingW * 2+            cellH = viewport._rH - paddingH * 2 -            widgetReqW =+            widgetW =               widget                 & _wnInfo                 & _wniSizeReqW-                & \r -> _szrFixed r + _szrFlex r+                & (\r -> _szrFixed r + _szrFlex r)+                & min cellW      render wenv node renderer = do-      forM_ (neighbours rowYs) $ \(y1, y2, even) -> do-        let color-              | mouseover && _pY mouse >= (t + y1) && _pY mouse < (t + y2) = Just mouseOverColor-              | not even = Just oddRowBgColor-              | otherwise = Nothing-        drawRect renderer (Rect l (t + y1) lastColX (y2 - y1)) color Nothing--      forM_ (S.drop 1 colXs) $ \colX -> do-        drawLine renderer (Point (l + colX) t) (Point (l + colX) (t + lastRowY)) 1 (Just lineColor)--      forM_ (S.drop 1 rowYs) $ \rowY -> do-        drawLine renderer (Point l (t + rowY)) (Point (l + lastColX) (t + rowY)) 1 (Just lineColor)+      drawRect renderer vp bgColor Nothing+      drawLine renderer (Point vp._rX (vp._rY + vp._rH)) (Point (vp._rX + vp._rW) (vp._rY + vp._rH)) 1 (Just lineColor)+      forM_ (drop 1 colXs) $ \colX -> do+        drawLine renderer (Point (vp._rX + colX) vp._rY) (Point (vp._rX + colX) (vp._rY + vp._rH)) 1 (Just lineColor)       where-        colXs = sizesToPositions (S.fromList (fromIntegral . currentWidth <$> model.columns))-        rowYs = sizesToPositions (toRowHeights (node ^. L.children) columnDefsSeq)-        lastColX-          | _ :|> a <- colXs = a-          | otherwise = 0-        lastRowY-          | _ :|> a <- rowYs = a-          | otherwise = 0+        colXs = scanl (+) 0 (fromIntegral <$> cpModel.columnWidths)+        bgColor+          | mouseover = Just mouseOverColor+          | rowIdx `mod` 2 == 1 = Just oddRowBgColor+          | otherwise = Nothing         vp = node ^. L.info . L.viewport-        Rect l t _w _h = vp         mouseover = pointInRect mouse vp         mouse = wenv ^. L.inputStatus . L.mousePos         mouseOverColor = (accentColor wenv) {_colorA = 0.3}         oddRowBgColor = (accentColor wenv) {_colorA = 0.1}         lineColor = accentColor wenv -    handleEvent _wenv node _path = \case-      Move (Point _pX _pY) ->-        -- refresh which row shows as hovered-        Just (resultReqs node [RenderOnce])-      _ -> Nothing--    handleMessage :: ContainerMessageHandler (HagridModel a) (HagridEvent ep)-    handleMessage wenv node _path msg = result-      where-        result = cast msg >>= handleTypedMessage--        handleTypedMessage (ContentPaneScrollToRow callback) = result-          where-            result-              | Just row <- callback indexedItems,-                Just y1 <- S.lookup row rowYs,-                Just y2 <- S.lookup (row + 1) rowYs =-                  Just (resultReqs node [SendMessage scrollId (ScrollTo (Rect vp._rX (vp._rY + y1) 1 (y2 - y1)))])-              | otherwise =-                  Nothing--            indexedItems =-              model.sortedItems-                & indexed-                & List.sortOn (snd . snd)-                & map (\(sortedIndex, (item, _originalIndex)) -> (item, sortedIndex))--            vp = node ^. L.info . L.viewport-            rowYs = sizesToPositions (toRowHeights (node ^. L.children) columnDefsSeq)--            scrollId = fromJust (widgetIdFromKey wenv (WidgetKey contentScrollKey))--initialModel :: [HagridCfg s e] -> [Column ep a] -> [a] -> HagridModel a-initialModel cfg columnDefs items = model+initialModel :: [HagridCfg s e] -> [Column ep a] -> Seq a -> HagridModel a+initialModel cfgs columnDefs items = model   where     model =       HagridModel-        { sortedItems = sortItems columnDefs sortColumn (zip items [0 ..]),+        { sortedItems = sortItems columnDefs sortColumn (S.zip items (S.fromFunction (length items) id)),           columns = initialColumn <$> columnDefs,-          sortColumn+          sortColumn,+          mdlEstimatedItemHeight = max 1 (fromMaybe 40 cfg.cfgEstimatedItemHeight)         } +    cfg = mconcat cfgs+     sortColumn-      | Just (col, dir) <- (mconcat cfg).cfgInitialSort,+      | Just (col, dir) <- cfg.cfgInitialSort,         col >= 0,         col < length columnDefs =           Just (col, dir)       | otherwise = Nothing -    initialColumn Column {name, initialWidth} =+    initialColumn Column {name, initialWidth, minWidth} =       ModelColumn         { name,-          currentWidth = initialWidth+          currentWidth = max minWidth initialWidth         } +-- | When the viewport position changes, this function computes the index and+-- position within the viewport of the new fixed row. This is the first row+-- whose y-position is at least minVisibleY+fixedRow :: Double -> Seq Double -> HagridModel a -> ContentPaneModel a -> (Int, Double)+fixedRow minVisibleY inflatedItemHeights model cpModel = (min maxRow row, offset)+  where+    (row, offset)+      | minVisibleY < inflatedStartY =+          let row = ceiling (minVisibleY / model.mdlEstimatedItemHeight)+              offset = (fromIntegral row * model.mdlEstimatedItemHeight) - minVisibleY+           in (row, offset)+      | otherwise =+          inflatedItem itemsBeforeInflated inflatedStartY inflatedItemHeights++    maxRow = length model.sortedItems - 1++    ContentPaneModel {itemsBeforeInflated} = cpModel++    inflatedStartY = fromIntegral itemsBeforeInflated * model.mdlEstimatedItemHeight++    inflatedItem i y = \case+      itemHeight :<| itemHeights+        | y + itemHeight >= minVisibleY ->+            (i + 1, (y + itemHeight) - minVisibleY)+        | otherwise ->+            inflatedItem (i + 1) (y + itemHeight) itemHeights+      S.Empty ->+        let indexInSection = ceiling ((minVisibleY - y) / model.mdlEstimatedItemHeight)+            row = i + indexInSection+            offset = (y + fromIntegral indexInSection * model.mdlEstimatedItemHeight) - minVisibleY+         in (row, offset)++resizeInnerRequest :: WidgetEnv s e -> EventResponse s e sp ep+resizeInnerRequest wenv =+  Request (ResizeWidgets (fromJust (widgetIdFromKey wenv (WidgetKey contentPaneInnerKey))))++itemsAfterInflated :: HagridModel a -> ContentPaneModel a -> Int+itemsAfterInflated model cpModel =+  length model.sortedItems - (cpModel.itemsBeforeInflated + length cpModel.inflatedItems)+ dragHandleWidth :: Int dragHandleWidth = 4 @@ -713,17 +962,24 @@ contentScrollKey :: Text contentScrollKey = "Hagrid.contentScroll" -contentPaneKey :: Text-contentPaneKey = "Hagrid.contentPane"+contentPaneOuterKey :: Text+contentPaneOuterKey = "Hagrid.contentPaneOuter" +contentPaneInnerKey :: Text+contentPaneInnerKey = "Hagrid.contentPaneInner"+ footerPaneKey :: Text footerPaneKey = "Hagrid.footerPane" -sortItems :: [Column ep a] -> Maybe (Int, SortDirection) -> [(a, Int)] -> [(a, Int)]+sortItems ::+  [Column ep a] ->+  Maybe (Int, SortDirection) ->+  Seq (ItemWithIndex a) ->+  Seq (ItemWithIndex a) sortItems columnDefs sortColumn items =   case modelSortKey columnDefs sortColumn of     DontSort -> items-    SortWith f -> List.sortOn (f . fst) items+    SortWith f -> S.sortOn (f . fst) items  modelSortKey :: [Column ep a] -> Maybe (Int, SortDirection) -> ColumnSortKey a modelSortKey columnDefs sortColumn = case sortColumn of@@ -735,11 +991,8 @@   _ ->     DontSort -sizesToPositions :: Seq Double -> Seq Double-sizesToPositions = S.scanl (+) 0--toRowHeights :: Seq (WidgetNode s e1) -> Seq (Column e2 a) -> Seq Double-toRowHeights children columnDefs = mergeHeights <$> S.chunksOf (length columnDefs) children+toRowHeight :: Seq (Column e2 a) -> Seq (WidgetNode s e1) -> Double+toRowHeight columnDefs = mergeHeights   where     mergeHeights rowWidgets =       foldl' max 0 (S.zipWith widgetHeight columnDefs rowWidgets)@@ -750,12 +1003,24 @@         & _wniSizeReqH         & \r -> _szrFixed r + _szrFlex r + paddingH * 2 -neighbours :: Seq a -> Seq (a, a, Bool)-neighbours = \case-  a :<| b :<| c :<| rest -> (a, b, False) :<| (b, c, True) :<| neighbours (c :<| rest)-  a :<| b :<| S.Empty -> S.singleton (a, b, False)-  _ -> S.empty+rowPositions :: forall s e. WidgetNode s e -> (Double, Seq Double, Double)+rowPositions node = (rowsStartY, rowHeights, rowsEndY)+  where+    vp = node._wnInfo._wniViewport+    childVps = _wniViewport . _wnInfo <$> node._wnChildren +    rowsStartY = case childVps of+      cvp :<| _ -> cvp._rY - vp._rY+      _ -> 0+    rowsEndY = case childVps of+      _ :|> cvp -> (cvp._rY - vp._rY) + cvp._rH+      _ -> 0+    rowHeights = _rH <$> childVps++takeAt :: Int -> Int -> Seq a -> Seq a+takeAt at len s =+  S.take len (S.drop at s)+ -- | Creates a column that displays a text value, and is sortable by the text. textColumn ::   -- | Name of the column, to display in the header.@@ -807,26 +1072,31 @@       sortHandler = Nothing     } -cellWidget :: (CompositeModel a, WidgetEvent e, WidgetModel s) => Int -> a -> ColumnWidget e a -> WidgetNode (HagridModel s) (HagridEvent e)+cellWidget ::+  (CompositeModel a, WidgetEvent e, WidgetModel s) =>+  Int ->+  a ->+  ColumnWidget e a ->+  WidgetNode (ContentPaneModel s) (ContentPaneEvent e) cellWidget idx item = \case   LabelWidget get -> label_ (get idx item) [ellipsis]   CustomWidget get -> widget     where       widget =         compositeD_ "Hagrid.Cell" (WidgetValue item) buildUI handleEvent []-      buildUI _wenv model =-        get idx model+      buildUI _wenv =+        get idx       handleEvent _wenv _node _model e =-        [Report (ParentEvent e)]+        [Report (ContentPaneParentEvent e)]  footerWidgetNode ::   (CompositeModel a, CompositeModel s, Typeable e) =>-  [(a, Int)] ->+  Seq (ItemWithIndex a) ->   ColumnFooterWidget e a ->-  WidgetNode (HagridModel s) (HagridEvent e)+  Maybe (WidgetNode (HagridModel s) (HagridEvent e)) footerWidgetNode items = \case-  NoFooterWidget -> spacer-  CustomFooterWidget get -> widget+  NoFooterWidget -> Nothing+  CustomFooterWidget get -> Just widget     where       widget =         compositeD_ "Hagrid.FooterCell" (WidgetValue items) buildUI handleEvent []@@ -837,6 +1107,10 @@  -- | Sends a message to the targeted 'hagrid' widget, that causes the -- widget to scroll such that a specified row becomes visible.+--+-- Note that this is inherently dynamically typed. If the type of the callback+-- does not match the type of the targeted hagrid widget then the message+-- will be ignored. scrollToRow ::   forall s e sp ep a.   (Typeable a, Typeable e) =>@@ -860,3 +1134,15 @@ flipSortDirection :: SortDirection -> SortDirection flipSortDirection SortAscending = SortDescending flipSortDirection SortDescending = SortAscending++ceilingDouble :: Double -> Double+ceilingDouble x = fromIntegral (ceiling x :: Int)++roundedRectEq :: Rect -> Rect -> Bool+roundedRectEq r1 r2 =+  roundedEq r1._rX r2._rX+    && roundedEq r1._rY r2._rY+    && roundedEq (r1._rX + r1._rW) (r2._rX + r2._rW)+    && roundedEq (r1._rY + r1._rH) (r2._rY + r2._rH)+  where+    roundedEq x y = (round x :: Int) == round y
test/Monomer/HagridSpec.hs view
@@ -3,6 +3,7 @@ import Control.Concurrent (newEmptyMVar, putMVar, takeMVar) import Control.Lens ((&), (.~), (^.)) import qualified Data.Foldable as Foldable+import qualified Data.Sequence as S import Data.Text (Text) import GHC.IO (unsafePerformIO) import Monomer@@ -61,6 +62,14 @@       [TestItem (fixedSize 10 & L.flex .~ 7)]       `shouldBe` [Rect 0 40 50 17] +  it "should have zero-height footer when no footer widgets specified" $+    viewports_+      [(testColumn "Col 1" (const (fixedSize 33))) {initialWidth = 50}]+      [TestItem (fixedSize 0)]+      []+      "Hagrid.FooterPane"+      `shouldBe` [Rect 0 100 100 0]+ sorting :: Spec sorting = describe "sorting" $ do   it "should not sort rows by default" $ do@@ -107,8 +116,8 @@ merging = describe "merging" $ do   it "should preserve column widths when items change" $ do     let col = textColumn "Col" (const "")-        startNode = nodeInit wenv (hagrid [col {initialWidth = 75}] [TestItem (fixedSize 1)])-        mergedNode = nodeMerge wenv (hagrid [col {initialWidth = 64}] [TestItem (fixedSize 2)]) startNode+        startNode = nodeInit wenv (hagrid [col {initialWidth = 75}] (S.fromList [TestItem (fixedSize 1)]))+        mergedNode = nodeMerge wenv (hagrid [col {initialWidth = 64}] (S.fromList [TestItem (fixedSize 2)])) startNode         resizedNode = nodeResize wenv mergedNode (mergedNode ^. L.info . L.viewport)     columnWidths startNode `shouldBe` [75]     columnWidths resizedNode `shouldBe` [75]@@ -120,10 +129,11 @@     let cols =           [(textColumn "Col" (const "")) {sortKey = SortWith (_szrFixed . sizeReq)}]         items =-          [ TestItem (fixedSize 1),-            TestItem (fixedSize 2),-            TestItem (fixedSize 3)-          ]+          S.fromList+            [ TestItem (fixedSize 1),+              TestItem (fixedSize 2),+              TestItem (fixedSize 3)+            ]         {-# NOINLINE scrollToRowCallback #-}         scrollToRowCallback cbItems =           unsafePerformIO (Nothing <$ putMVar itemsMVar cbItems) -- hacky, but seems to work!@@ -137,14 +147,15 @@           nodeHandleEventEvts wenv [] cmpNode     actualItems <- seq evts (takeMVar itemsMVar)     actualItems-      `shouldBe` [ (TestItem (fixedSize 1), 2),-                   (TestItem (fixedSize 2), 1),-                   (TestItem (fixedSize 3), 0)-                 ]+      `shouldBe` S.fromList+        [ (TestItem (fixedSize 1), 2),+          (TestItem (fixedSize 2), 1),+          (TestItem (fixedSize 3), 0)+        ]  testColumn :: Text -> (TestItem -> SizeReq) -> Column TestEvent TestItem testColumn name getHeight =-  (widgetColumn name (testCellWidget getHeight)) {paddingW = 0, paddingH = 0}+  (widgetColumn name (testCellWidget getHeight)) {minWidth = 10, paddingW = 0, paddingH = 0}  -- | We test with custom widgets because these will create special "Hagrid.Cell" nodes in the widget -- tree that we can later use to pick out the cell widgets.@@ -160,20 +171,23 @@   cellViewports_ columnDefs items []  cellViewports_ :: [Column TestEvent TestItem] -> [TestItem] -> [SystemEvent] -> [Rect]-cellViewports_ columnDefs items evts = Foldable.toList childVps+cellViewports_ columnDefs items evts = viewports_ columnDefs items evts "Hagrid.Cell"++viewports_ :: [Column TestEvent TestItem] -> [TestItem] -> [SystemEvent] -> WidgetType -> [Rect]+viewports_ columnDefs items evts wType = Foldable.toList childVps   where-    startNode = nodeInit wenv (hagrid columnDefs items)+    startNode = nodeInit wenv (hagrid columnDefs (S.fromList items))     ((wenv', eventedNode, _reqs), _) = nodeHandleEvents wenv WNoInit evts startNode     resizedNode = nodeResize wenv' eventedNode (eventedNode ^. L.info . L.viewport)     instanceTree = widgetGetInstanceTree (resizedNode ^. L.widget) wenv' resizedNode-    childVps = roundRectUnits . _wniViewport . _winInfo <$> widgetsOfType "Hagrid.Cell" instanceTree+    childVps = roundRectUnits . _wniViewport . _winInfo <$> widgetsOfType wType instanceTree  -- Extract the column widths by observing the locations of the special drag handle widgets columnWidths :: WidgetNode TestModel TestEvent -> [Int] columnWidths node = fromFractional <$> colWidths   where     (_, colWidths) =-      Foldable.foldl' (\(px, cws) (Rect x _y w _h) -> (x + w, (x + w - px) : cws)) (0, []) vps+      Foldable.foldl' (\(px, cws) (Rect x _y w _h) -> (x + w, (x + w / 2 - px) : cws)) (0, []) vps     vps =       _wniViewport . _winInfo <$> dragHandles     dragHandles =