packages feed

monomer 1.4.0.0 → 1.4.1.0

raw patch · 23 files changed

+389/−108 lines, 23 files

Files

ChangeLog.md view
@@ -1,3 +1,23 @@+## 1.4.1.0++### Added++- Support for switching vertical wheel scrolling to horizontal in scroll widget by pressing the shift key ([PR #137](https://github.com/fjvallarino/monomer/pull/137)).+- Drawing and theme utility functions ([PR #138](https://github.com/fjvallarino/monomer/pull/138)).+- `boxFilterEvent` config option, exposing Container's filterEvent functionality ([PR #146](https://github.com/fjvallarino/monomer/pull/146)).+- `pointToLineDistance` utility function ([PR #150](https://github.com/fjvallarino/monomer/pull/150)).++### Fixed++- Bug where memory based image widget would not render their new state after a merge ([PR #147](https://github.com/fjvallarino/monomer/pull/147)). Thanks @CamdenKuwahara!+- Fix memory leak in NanoVGRenderer's image initialization. Dispose unused images in image widget ([PR #149](https://github.com/fjvallarino/monomer/pull/149)).++### Changed++- Restricts toggleButton's type argument for configuration options to Bool (the only valid type) ([PR #150](https://github.com/fjvallarino/monomer/pull/150)).+- Avoids drawing borders if width == 0 ([PR #150](https://github.com/fjvallarino/monomer/pull/150)).+- Updates default style for tooltip widget ([PR #150](https://github.com/fjvallarino/monomer/pull/150)).+ ## 1.4.0.0  ### Breaking changes
monomer.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack  name:           monomer-version:        1.4.0.0+version:        1.4.1.0 synopsis:       A GUI library for writing native Haskell applications. description:    Monomer is an easy to use, cross platform, GUI library for writing native                 Haskell applications.
src/Monomer/Common/BasicTypes.hs view
@@ -101,6 +101,18 @@ negPoint :: Point -> Point negPoint (Point x y) = Point (-x) (-y) +{-|+Returns the minimum distance from the point given as first argument to the line+formed by the points given as second and third arguments.+-}+pointToLineDistance :: Point -> Point -> Point -> Double+pointToLineDistance p0 p1 p2 = distNum / distDen where+  Point px0 py0 = p0+  Point px1 py1 = p1+  Point px2 py2 = p2+  distNum = abs ((px2 - px1) * (py1 - py0) - (px1 - px0) * (py2 - py1))+  distDen = sqrt ((px2 - px1) ** 2 + (py2 - py1) ** 2)+ -- | Checks if a coordinate is inside the horizontal range of a rect. coordInRectH :: Double -> Rect -> Bool coordInRectH px (Rect x y w h) = px >= x && px < x + w
src/Monomer/Core/ThemeTypes.hs view
@@ -9,7 +9,7 @@ Theme configuration types. -} {-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE Strict #-}+{-# LANGUAGE StrictData #-}  module Monomer.Core.ThemeTypes where @@ -19,7 +19,6 @@  import qualified Data.Map.Strict as M -import Monomer.Common import Monomer.Core.StyleTypes import Monomer.Graphics.ColorTable import Monomer.Graphics.Types
src/Monomer/Core/Themes/BaseTheme.hs view
@@ -215,7 +215,8 @@   & L.text . non def . L.fontColor ?~ tooltipText themeMod   & L.bgColor ?~ tooltipBg themeMod   & L.border ?~ border 1 (tooltipBorder themeMod)-  & L.padding ?~ padding 4+  & L.padding ?~ padding 6+  & L.radius ?~ radius 4  baseBasic :: BaseThemeColors -> ThemeState baseBasic themeMod = def
src/Monomer/Core/Themes/SampleThemes.hs view
@@ -117,8 +117,8 @@   slSelectedText = black,   slSelectedFocusBorder = blue07, -  tooltipBorder = gray09,-  tooltipBg = gray10,+  tooltipBorder = gray08,+  tooltipBg = gray07,   tooltipText = black } @@ -216,8 +216,8 @@   slSelectedText = white,   slSelectedFocusBorder = blue08, -  tooltipBorder = gray09,-  tooltipBg = gray04,+  tooltipBorder = gray05,+  tooltipBg = rgbHex "#1D212B",   tooltipText = white } 
src/Monomer/Core/Util.hs view
@@ -31,19 +31,19 @@ import qualified Monomer.Core.Lens as L  -- | Returns the 'Path' associated to a given 'WidgetKey', if any. The search is---   restricted to the parent _Composite_.+--   restricted to the parent "Monomer.Widgets.Composite". pathFromKey :: WidgetEnv s e -> WidgetKey -> Maybe Path pathFromKey wenv key = fmap (^. L.info . L.path) node where   node = Map.lookup key (wenv ^. L.widgetKeyMap)  -- | Returns the 'WidgetId' associated to a given 'WidgetKey', if any. The---   search is restricted to the parent _Composite_.+--   search is restricted to the parent "Monomer.Widgets.Composite". widgetIdFromKey :: WidgetEnv s e -> WidgetKey -> Maybe WidgetId widgetIdFromKey wenv key = fmap (^. L.info . L.widgetId) node where   node = Map.lookup key (wenv ^. L.widgetKeyMap)  -- | Returns the 'WidgetNodeInfo' associated to the given 'WidgetKey', if any.---   The search is restricted to the parent _Composite_.+--   The search is restricted to the parent "Monomer.Widgets.Composite". nodeInfoFromKey :: WidgetEnv s e -> WidgetKey -> Maybe WidgetNodeInfo nodeInfoFromKey wenv key = path >>= nodeInfoFromPath wenv where   path = pathFromKey wenv key
src/Monomer/Core/WidgetTypes.hs view
@@ -68,7 +68,7 @@   | FocusBwd  -- ^ Focus moving backward (usually right to left, top to bottom).   deriving (Eq, Show) --- | "WidgetRequest" specific for window related operations.+-- | 'WidgetRequest' specific for window related operations. data WindowRequest   = WindowSetTitle Text  -- ^ Sets the title of the window to the given text.   | WindowSetFullScreen  -- ^ Switches to fullscreen mode.@@ -216,9 +216,12 @@   -- | Requests to exit the application. Can also be used to cancel a previous   --   request (or a window close).   | ExitApplication Bool-  -- | Performs a "WindowRequest".+  -- | Performs a 'WindowRequest'.   | UpdateWindow WindowRequest-  -- | Request a model update. This usually involves lenses and "widgetDataSet".+  {-|+  Request a model update. This usually involves lenses and+  'Monomer.Widgets.Util.Widget.widgetDataSet'.+  -}   | UpdateModel (s -> s)   -- | Updates the path of a given widget. Both "Monomer.Widgets.Single" and   --   "Monomer.Widgets.Container" handle this automatically.@@ -273,11 +276,13 @@  {-| Result of widget operations (init, merge, handleEvent, etc). The node is-mandatory. The "resultNode", "resultEvts", "resultReqs" and "resultReqsEvts"-helper functions can also be used.+mandatory. The 'Monomer.Widgets.Util.Widget.resultNode',+'Monomer.Widgets.Util.Widget.resultEvts',+'Monomer.Widgets.Util.Widget.resultReqs' and+'Monomer.Widgets.Util.Widget.resultReqsEvts' helper functions can also be used.  In general a result starts in a child widget, but parent widgets can append-requets or new versions of themselves.+requests or new versions of themselves. -} data WidgetResult s e = WidgetResult {   _wrNode :: WidgetNode s e,              -- ^ The updated widget node.@@ -322,10 +327,12 @@   _weWindowSize :: Size,   -- | The active map of shared data.   _weWidgetShared :: MVar (Map Text WidgetShared),-  {-+  {-|   The active map of WidgetKey -> WidgetNode, if any. This map is restricted to-  to the parent 'Composite'. Do not use this map directly, rely instead on the-  'widgetIdFromKey', 'nodeInfoFromKey' and 'nodeInfoFromPath' utility functions.+  to the parent "Monomer.Widgets.Composite". Do not use this map directly, rely+  instead on the 'Monomer.Core.Util.widgetIdFromKey',+  'Monomer.Core.Util.nodeInfoFromKey' and 'Monomer.Core.Util.nodeInfoFromPath'+  utility functions.   -}   _weWidgetKeyMap :: WidgetKeyMap s e,   -- | The currently hovered path, if any.@@ -625,8 +632,8 @@     Receives a message and, optionally, returns a result. This can include an     updated version of the widget (in case it has internal state), user events     or requests to the runtime. There is no validation regarding the message-    type, and the widget should take care of _casting_ to the correct type using-    "Data.Typeable.cast"+    type, and the widget should take care of /casting/ to the correct type using+    'Data.Typeable.cast'.      Arguments: 
src/Monomer/Graphics/NanoVGRenderer.hs view
@@ -471,7 +471,7 @@  imgInsertNew :: Text -> ImageDef -> ImagesMap -> VG.Image -> ImagesMap imgInsertNew name imageDef imagesMap nvImg = newImagesMap where-  image = Image imageDef nvImg 0+  image = Image imageDef nvImg 1   newImagesMap = M.insert name image imagesMap  imgDelete :: Text -> ImagesMap -> ImagesMap
src/Monomer/Widgets/Composite.hs view
@@ -170,14 +170,14 @@   = Model s   -- | Raises a new event, which will be handled in the same cycle.   | Event e-  -- | Raises an event that will be handled by the parent.+  -- | Raises an event that will be handled by the parent Composite.   | Report ep   -- | Generates a 'WidgetRequest'.   | Request (WidgetRequest s e)   {-|-  Generates a 'WidgetRequest' matching the parent's types. Useful when receiving-  requests as configuration from the parent, since the types will not match-  otherwise.+  Generates a 'WidgetRequest' matching the parent Composite's type. Useful when+  receiving requests as configuration from the parent, since the types will not+  match otherwise.   -}   | RequestParent (WidgetRequest sp ep)   {-|
src/Monomer/Widgets/Container.hs view
@@ -258,7 +258,7 @@  {-| Receives a System event and, optionally, modifies the event, its target, or-cancels the event propagation by returning null.+stops the event propagation by returning Nothing.  Examples can be found in "Monomer.Widgets.Containers.Base.LabeledItem". -}
src/Monomer/Widgets/Containers/Box.hs view
@@ -58,7 +58,8 @@   -- * Constructors   box,   box_,-  expandContent+  expandContent,+  boxFilterEvent ) where  import Control.Applicative ((<|>))@@ -101,11 +102,13 @@ - 'onClickEmptyReq': generates a WidgetRequest on click in empty area. - 'expandContent': if the inner widget should use all the available space. To be   able to use alignment options, this must be False (the default).+- 'boxFilterEvent': allows filtering or modifying a 'SystemEvent'. -} data BoxCfg s e = BoxCfg {   _boxExpandContent :: Maybe Bool,   _boxIgnoreEmptyArea :: Maybe Bool,   _boxSizeReqUpdater :: [SizeReqUpdater],+  _boxFilterEvent :: Maybe (ContainerFilterHandler s e),   _boxMergeRequired :: Maybe (WidgetEnv s e -> s -> s -> Bool),   _boxAlignH :: Maybe AlignH,   _boxAlignV :: Maybe AlignV,@@ -124,6 +127,7 @@     _boxExpandContent = Nothing,     _boxIgnoreEmptyArea = Nothing,     _boxSizeReqUpdater = [],+    _boxFilterEvent = Nothing,     _boxMergeRequired = Nothing,     _boxAlignH = Nothing,     _boxAlignV = Nothing,@@ -142,6 +146,7 @@     _boxExpandContent = _boxExpandContent t2 <|> _boxExpandContent t1,     _boxIgnoreEmptyArea = _boxIgnoreEmptyArea t2 <|> _boxIgnoreEmptyArea t1,     _boxSizeReqUpdater = _boxSizeReqUpdater t1 <> _boxSizeReqUpdater t2,+    _boxFilterEvent = _boxFilterEvent t2 <|> _boxFilterEvent t1,     _boxMergeRequired = _boxMergeRequired t2 <|> _boxMergeRequired t1,     _boxAlignH = _boxAlignH t2 <|> _boxAlignH t1,     _boxAlignV = _boxAlignV t2 <|> _boxAlignV t1,@@ -295,6 +300,15 @@   _boxExpandContent = Just True } +{-|+Receives a System event and, optionally, modifies the event, its target, or+stops the event propagation by returning Nothing.+-}+boxFilterEvent :: ContainerFilterHandler s e -> BoxCfg  s e+boxFilterEvent handler = def {+  _boxFilterEvent = Just handler+}+ newtype BoxState s = BoxState {   _bxsModel :: Maybe s }@@ -330,6 +344,7 @@     containerInit = init,     containerMergeChildrenReq = mergeRequired,     containerMerge = merge,+    containerFilterEvent = filterEvent,     containerHandleEvent = handleEvent,     containerGetSizeReq = getSizeReq,     containerResize = resize@@ -357,6 +372,10 @@   getCurrentStyle = currentStyle_ currentStyleConfig where     currentStyleConfig = def       & L.isActive .~ isNodeTreeActive++  filterEvent = case _boxFilterEvent config of+    Just handler -> handler+    _ -> \wenv node target evt -> Just (target, evt)    handleEvent wenv node target evt = case evt of     Focus prev -> handleFocusChange node prev (_boxOnFocusReq config)
src/Monomer/Widgets/Containers/Scroll.hs view
@@ -588,8 +588,12 @@       result         | needsUpdate = Just $ makeResult newState         | otherwise = Nothing-      stepX = wheelRate * wx-      stepY = wheelRate * wy+      stepX+        | shiftPressed && changedY = wheelRate * wy+        | otherwise = wheelRate * wx+      stepY+        | shiftPressed = 0+        | otherwise = wheelRate * wy       newState = state {         _sstDeltaX = scrollAxisH (stepX + dx),         _sstDeltaY = scrollAxisV (stepY + dy)@@ -601,6 +605,7 @@       style = scrollCurrentStyle wenv node       contentArea = getContentArea node style       mousePos = wenv ^. L.inputStatus . L.mousePos+      shiftPressed = isShiftPressed (wenv ^. L.inputStatus . L.keyMod)        Rect cx cy cw ch = contentArea       sctx = scrollStatus config wenv node state mousePos
src/Monomer/Widgets/Containers/Tooltip.hs view
@@ -11,8 +11,8 @@  Tooltip styling is a bit unusual, since it is applied to the overlaid element. This means padding will not be shown for the contained child element, but only-on the message when the tooltip is active. If you need padding around the child-element, you can use a "Monomer.Widgets.Containers.Box" around it.+on the message when the tooltip is active. If padding around the child element+is needed, "Monomer.Widgets.Containers.Box" can be used to wrap it.  @ tooltip "Click the button" (buttom \"Accept\" AcceptAction)
src/Monomer/Widgets/Singles/Button.hs view
@@ -59,6 +59,7 @@ - 'ellipsis': if ellipsis should be used for overflown text. - 'multiline': if text may be split in multiple lines. - 'maxLines': maximum number of text lines to show.+- 'ignoreTheme': whether to load default style from theme or start empty. - 'resizeFactor': flexibility to have more or less spaced assigned. - 'resizeFactorW': flexibility to have more or less horizontal spaced assigned. - 'resizeFactorH': flexibility to have more or less vertical spaced assigned.@@ -184,14 +185,14 @@  {-| Creates a button with main styling. Useful to highlight an option, such as-"Accept", when multiple buttons are available.+\"Accept\", when multiple buttons are available. -} mainButton :: WidgetEvent e => Text -> e -> WidgetNode s e mainButton caption handler = button_ caption handler [mainConfig]  {-| Creates a button with main styling. Useful to highlight an option, such as-"Accept", when multiple buttons are available. Accepts config.+\"Accept\", when multiple buttons are available. Accepts config. -} mainButton_ :: WidgetEvent e => Text -> e -> [ButtonCfg s e] -> WidgetNode s e mainButton_ caption handler configs = button_ caption handler newConfigs where@@ -199,7 +200,7 @@  {-| Creates a button with main styling. Useful to highlight an option, such as-"Accept", when multiple buttons are available. Accepts config but does not+\"Accept\", when multiple buttons are available. Accepts config but does not require an event. See 'buttonD_'. -} mainButtonD_ :: WidgetEvent e => Text -> [ButtonCfg s e] -> WidgetNode s e@@ -220,9 +221,10 @@ use this version, adding an 'onClick' handler in configs.  Using this constructor directly can be helpful in cases where the event to be-raised belongs in a _Composite_ above in the widget tree, outside the scope of-the Composite that contains the button. This parent Composite can be reached by-sending a message ('SendMessage') to its 'WidgetId' using 'onClickReq'.+raised belongs in a "Monomer.Widgets.Composite" above in the widget tree,+outside the scope of the Composite that contains the button. This parent+Composite can be reached by sending a message ('SendMessage') to its 'WidgetId'+using 'onClickReq'. -} buttonD_ :: WidgetEvent e => Text -> [ButtonCfg s e] -> WidgetNode s e buttonD_ caption configs = buttonNode where
src/Monomer/Widgets/Singles/Image.hs view
@@ -48,7 +48,7 @@ import Data.ByteString (ByteString) import Data.Char (toLower) import Data.Default-import Data.Map (Map)+import Data.Map.Strict (Map) import Data.Maybe import Data.List (isPrefixOf) import Data.Text (Text)@@ -62,7 +62,7 @@ import qualified Codec.Picture as Pic import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as BSL-import qualified Data.Map as Map+import qualified Data.Map.Strict as Map import qualified Data.Text as T import qualified Network.Wreq.Session as Sess @@ -303,7 +303,7 @@     singleRender = render   } -  !isImageMem = case imgSource of+  isImageMem !source = case source of     ImageMem{} -> True     _ -> False @@ -324,20 +324,30 @@   merge wenv newNode oldNode oldState = result where     wid = newNode ^. L.info . L.widgetId     path = newNode ^. L.info . L.path+     oldSource = isImageSource oldState-    imgPath = imgName imgSource-    prevPath = imgName oldSource +    newPath = imgName imgSource+    oldPath = imgName oldSource+    isNewMem = isImageMem imgSource+    isOldMem = isImageMem oldSource+     sameImgNode = newNode       & L.widget .~ makeImage imgSource config oldState-    newMemReqs = [ RemoveRendererImage prevPath ]-    newImgReqs = [-        RemoveRendererImage prevPath,-        RunTask wid path (handleImageLoad config wenv imgPath)++    disposeOld = [ RunTask wid path (handleImageDispose wenv oldPath) | not isOldMem ]++    newMemReqs = disposeOld ++ [+        RemoveRendererImage oldPath,+        ResizeWidgets wid       ]+    newImgReqs = disposeOld ++ [+        RemoveRendererImage oldPath,+        RunTask wid path (handleImageLoad config wenv newPath)+      ]     result       | oldSource == imgSource = resultNode sameImgNode-      | isImageMem = resultReqs newNode newMemReqs+      | isNewMem = resultReqs newNode newMemReqs       | otherwise = resultReqs newNode newImgReqs    dispose wenv node = resultReqs node reqs where@@ -458,6 +468,7 @@   -- loading images in parallel. The main MVar is only taken until the image's   -- MVar is retrieved/created.   (sharedMap, sess) <- takeMVar sharedMapMVar >>= getImagesSession+   sharedImgMVar <- case useShared (Map.lookup key sharedMap) of     Just mvar -> return mvar     Nothing -> newMVar emptyImgState@@ -465,6 +476,7 @@    -- Take the image's MVar until done   sharedImg <- takeMVar sharedImgMVar+   (result, newSharedImg) <- case sharedImg of     Just (oldState, oldCount) -> do       return (ImageLoaded oldState, Just (oldState, oldCount + 1))@@ -488,15 +500,18 @@ handleImageDispose :: WidgetEnv s e -> Text -> IO () handleImageDispose wenv path = do   sharedMap <- takeMVar sharedMapMVar+   newSharedMap <- case useShared (Map.lookup key sharedMap) of     Just mvar -> do       sharedImg <- takeMVar mvar+       return $ case sharedImg of         Just (oldState :: ImageState, oldCount :: Int)           | oldCount > 1 ->               sharedMap & at key ?~ WidgetShared (oldState, oldCount - 1)         _ -> sharedMap & at key .~ Nothing     Nothing -> return sharedMap+   putMVar sharedMapMVar newSharedMap   where     sharedMapMVar = wenv ^. L.widgetShared
src/Monomer/Widgets/Singles/Label.hs view
@@ -17,7 +17,7 @@ Multi-line label:  @-label_ "This is a\nmultiline label" [multiline]+label_ "This is a\\nmultiline label" [multiline] @ -} {-# LANGUAGE DeriveGeneric #-}
src/Monomer/Widgets/Singles/ToggleButton.hs view
@@ -67,10 +67,10 @@ - 'onChange': event to raise when the value changes. - 'onChangeReq': 'WidgetRequest' to generate when the value changes. -}-type ToggleButtonCfg = OptionButtonCfg+type ToggleButtonCfg s e = OptionButtonCfg s e Bool  -- | Sets the style for the Off state of the toggle button.-toggleButtonOffStyle :: Style -> ToggleButtonCfg s e a+toggleButtonOffStyle :: Style -> ToggleButtonCfg s e toggleButtonOffStyle = optionButtonOffStyle  -- | Creates a toggleButton using the given lens.@@ -84,7 +84,7 @@ toggleButton_   :: Text   -> ALens' s Bool-  -> [ToggleButtonCfg s e Bool]+  -> [ToggleButtonCfg s e]   -> WidgetNode s e toggleButton_ caption field cfgs = newNode where   newNode = toggleButtonD_ caption (WidgetLens field) cfgs@@ -106,7 +106,7 @@   => Text   -> Bool   -> (Bool -> e)-  -> [ToggleButtonCfg s e Bool]+  -> [ToggleButtonCfg s e]   -> WidgetNode s e toggleButtonV_ caption value handler configs = newNode where   widgetData = WidgetValue value@@ -117,7 +117,7 @@ toggleButtonD_   :: Text   -> WidgetData s Bool-  -> [ToggleButtonCfg s e Bool]+  -> [ToggleButtonCfg s e]   -> WidgetNode s e toggleButtonD_ caption widgetData configs = toggleButtonNode where   config = mconcat configs
src/Monomer/Widgets/Util/Drawing.hs view
@@ -22,6 +22,8 @@   drawLine,   drawRect,   drawRectBorder,+  drawTriangle,+  drawTriangleBorder,   drawArc,   drawArcBorder,   drawEllipse,@@ -205,6 +207,49 @@ drawRectBorder renderer rect border (Just radius) =   drawRoundedRectBorder renderer rect border radius +{-|+Draws a filled triangle with the given color.++The points should be provided in clockwise order.+-}+drawTriangle+  :: Renderer     -- ^ The renderer.+  -> Point        -- ^ The first point.+  -> Point        -- ^ The second point.+  -> Point        -- ^ The third point.+  -> Maybe Color  -- ^ The color. If Nothing, the triangle will not be drawn.+  -> IO ()        -- ^ The resulting action.+drawTriangle _ _ _ _ Nothing = return ()+drawTriangle renderer p1 p2 p3 (Just color) = do+  beginPath renderer+  setFillColor renderer color+  moveTo renderer p1+  renderLineTo renderer p2+  renderLineTo renderer p3+  closePath renderer+  fill renderer++-- | Draws a triangle's border with the given color and width.+drawTriangleBorder+  :: Renderer     -- ^ The renderer.+  -> Point        -- ^ The first point.+  -> Point        -- ^ The second point.+  -> Point        -- ^ The third point.+  -> Double       -- ^ The border width.+  -> Maybe Color  -- ^ The color. If Nothing, the triangle will not be drawn.+  -> IO ()        -- ^ The resulting action.+drawTriangleBorder _ _ _ _ _ Nothing = return ()+drawTriangleBorder renderer p1 p2 p3 width (Just color) =+  when (width > 0) $ do+    beginPath renderer+    setStrokeColor renderer color+    setStrokeWidth renderer width+    moveTo renderer p1+    renderLineTo renderer p2+    renderLineTo renderer p3+    closePath renderer+    stroke renderer+ -- | Draws a filled arc, delimited by a rect and within the given angles. drawArc   :: Renderer      -- ^ The renderer.@@ -236,12 +281,13 @@   -> Double        -- ^ The arc width.   -> IO ()         -- ^ The resulting action. drawArcBorder renderer rect start end winding Nothing width = return ()-drawArcBorder renderer rect start end winding (Just color) width = do-  beginPath renderer-  setStrokeColor renderer color-  setStrokeWidth renderer width-  renderArc renderer center radius start end winding-  stroke renderer+drawArcBorder renderer rect start end winding (Just color) width =+  when (width > 0) $ do+    beginPath renderer+    setStrokeColor renderer color+    setStrokeWidth renderer width+    renderArc renderer center radius start end winding+    stroke renderer   where     Rect rx ry rw rh = rect     radius = min ((rw - width) / 2) ((rh - width) / 2)@@ -269,12 +315,13 @@   -> IO ()         -- ^ The resulting action. drawEllipseBorder renderer rect Nothing _ = return () drawEllipseBorder renderer rect (Just color) width =-  forM_ contentRect $ \finalRect -> do-    beginPath renderer-    setStrokeColor renderer color-    setStrokeWidth renderer width-    renderEllipse renderer finalRect-    stroke renderer+  when (width > 0) $+    forM_ contentRect $ \finalRect -> do+      beginPath renderer+      setStrokeColor renderer color+      setStrokeWidth renderer width+      renderEllipse renderer finalRect+      stroke renderer   where     contentRect = subtractFromRect rect w w w w     w = width / 2@@ -437,19 +484,21 @@ drawRectCorner _ _ ocorner Nothing Nothing = return points where   points = (ocorner, ocorner, ocorner) drawRectCorner renderer cor ocorner ms1 ms2 = do-  beginPath renderer+  when (w1 > 0 && w2 > 0) $ do+    beginPath renderer -  if color1 == color2-    then setFillColor renderer color1-    else setFillLinearGradient renderer g1 g2 color1 color2+    if color1 == color2+      then setFillColor renderer color1+      else setFillLinearGradient renderer g1 g2 color1 color2 -  moveTo renderer o1-  renderLineTo renderer icorner-  renderLineTo renderer o2-  renderLineTo renderer ocorner-  closePath renderer+    moveTo renderer o1+    renderLineTo renderer icorner+    renderLineTo renderer o2+    renderLineTo renderer ocorner+    closePath renderer -  fill renderer+    fill renderer+   return (o1, o2, icorner)   where     Point cx cy = ocorner@@ -530,30 +579,31 @@ drawRoundedCorner _ _ _ center _ Nothing Nothing = return points where   points = (center, center, center, center) drawRoundedCorner renderer cor bounds ocenter mrcor ms1 ms2 = do-  beginPath renderer+  when (w1 > 0 || w2 > 0) $ do+    beginPath renderer -  if color1 == color2-    then setFillColor renderer color1-    else setFillLinearGradient renderer g1 g2 color1 color2+    if color1 == color2+      then setFillColor renderer color1+      else setFillLinearGradient renderer g1 g2 color1 color2 -  if round orad == 0-    then drawRectArc renderer cor icenter w1 w2-    else renderArc renderer ocenter orad deg (deg - 90) CCW+    if round orad == 0+      then drawRectArc renderer cor icenter w1 w2+      else renderArc renderer ocenter orad deg (deg - 90) CCW -  renderLineTo renderer o1+    renderLineTo renderer o1 -  if round orad > 0 && round irad > 0-    then do-      renderLineTo renderer i1-      renderArc renderer icenter irad (deg - 90) deg CW-      renderLineTo renderer i2-    else do-      renderLineTo renderer icenter+    if round orad > 0 && round irad > 0+      then do+        renderLineTo renderer i1+        renderArc renderer icenter irad (deg - 90) deg CW+        renderLineTo renderer i2+      else do+        renderLineTo renderer icenter -  renderLineTo renderer o2+    renderLineTo renderer o2 -  closePath renderer-  fill renderer+    closePath renderer+    fill renderer    return bordersCorners   where@@ -619,15 +669,16 @@  drawQuad :: Renderer -> Point -> Point -> Point -> Point -> Maybe BorderSide -> IO () drawQuad renderer p1 p2 p3 p4 Nothing = pure ()-drawQuad renderer p1 p2 p3 p4 (Just BorderSide{..}) = do-  beginPath renderer-  setFillColor renderer _bsColor-  moveTo renderer p1-  renderLineTo renderer p2-  renderLineTo renderer p3-  renderLineTo renderer p4-  closePath renderer-  fill renderer+drawQuad renderer p1 p2 p3 p4 (Just BorderSide{..}) =+  when (_bsWidth > 0) $ do+    beginPath renderer+    setFillColor renderer _bsColor+    moveTo renderer p1+    renderLineTo renderer p2+    renderLineTo renderer p3+    renderLineTo renderer p4+    closePath renderer+    fill renderer  cornerGradientPoints :: Point -> Point -> (Point, Point) cornerGradientPoints outer inner = (g1, g2) where
src/Monomer/Widgets/Util/Theme.hs view
@@ -8,6 +8,7 @@  Helper functions for loading theme values. -}+{-# LANGUAGE BangPatterns #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE Strict #-} @@ -73,3 +74,84 @@   active = wenv ^. L.theme . L.active . L.userStyleMap . at name   disabled = wenv ^. L.theme . L.disabled . L.userStyleMap . at name   style = Style basic hover focus focusHover active disabled++{-|+Sets the given value, overwriting the previous one, for each of the states of a+theme.++Useful for customizing a base theme.++@+theme = darkTheme+  & setThemeValue L.sliderRadius (Just 10)+  & setThemeValue L.sliderWidth 10+@+-}+setThemeValue+  :: Lens' ThemeState a+  -> a+  -> Theme+  -> Theme+setThemeValue field value theme = newTheme where+  newTheme = theme+    & L.basic . field .~ value+    & L.hover . field .~ value+    & L.focus . field .~ value+    & L.focusHover . field .~ value+    & L.active . field .~ value+    & L.disabled . field .~ value++{-|+Sets the given style options, overwriting the previous style, for each of the+states of a theme.++Useful for customizing a base theme.++@+theme = darkTheme+  & setThemeStyle L.dropdownStyle [padding 6, paddingV 6]+@++Note: In most cases 'mergeThemeStyle' is a better choice, since it keeps all the+other style settings intact.+-}+setThemeStyle+  :: Lens' ThemeState StyleState+  -> [StyleState]+  -> Theme+  -> Theme+setThemeStyle field styles theme = newTheme where+  !value = mconcat styles+  newTheme = theme+    & L.basic . field .~ value+    & L.hover . field .~ value+    & L.focus . field .~ value+    & L.focusHover . field .~ value+    & L.active . field .~ value+    & L.disabled . field .~ value++{-|+Merges the given style options with the existing style, for each of the states+of a theme.++Useful for customizing a base theme.++@+theme = darkTheme+  & mergeThemeStyle L.dropdownStyle [padding 6, paddingV 6]+@+-}+mergeThemeStyle+  :: Lens' ThemeState StyleState+  -> [StyleState]+  -> Theme+  -> Theme+mergeThemeStyle field styles theme = newTheme where+  !value = mconcat styles+  newTheme = theme+    & L.basic . field <>~ value+    & L.hover . field <>~ value+    & L.focus . field <>~ value+    & L.focusHover . field <>~ value+    & L.active . field <>~ value+    & L.disabled . field <>~ value
test/unit/Monomer/Widgets/Containers/BoxSpec.hs view
@@ -26,6 +26,7 @@ import Monomer.TestEventUtil import Monomer.TestUtil import Monomer.Widgets.Containers.Box+import Monomer.Widgets.Containers.Grid import Monomer.Widgets.Containers.ZStack import Monomer.Widgets.Singles.Button import Monomer.Widgets.Singles.Label@@ -45,6 +46,7 @@ spec :: Spec spec = describe "Box" $ do   mergeReq+  filterEvent   handleEvent   handleEventIgnoreEmpty   handleEventSinkEmpty@@ -74,6 +76,29 @@     mergeWith newNode oldNode = result ^?! L.node . L.children . ix 0 where       oldNode2 = nodeInit wenv oldNode       result = widgetMerge (newNode ^. L.widget) wenv newNode oldNode2++filterEvent :: Spec+filterEvent = describe "filterEvent" $ do+  it "should click the first button, since the click event was directed to it" $+    evts False [evtClick point1] `shouldBe` Seq.singleton (BtnClick 1)++  it "should click the second button, since the click event's target was modified" $+    evts True [evtClick point1] `shouldBe` Seq.singleton (BtnClick 2)++  where+    wenv = mockWenv ()+    point1 = Point 320 200+    point2 = Point 320 400++    filterEvent wenv node target evt = Just (target & ix 2 .~ 1, evtClick point2)++    btnBox ignore = box_ (expandContent : [boxFilterEvent filterEvent | ignore]) $+      vgrid [+        button "Button 1" (BtnClick 1),+        button "Button 2" (BtnClick 2)+      ]+    evts ignore es = nodeHandleEventEvts wenv es $+      nodeInit wenv (btnBox ignore)  handleEvent :: Spec handleEvent = describe "handleEvent" $ do
test/unit/Monomer/Widgets/Containers/ScrollSpec.hs view
@@ -53,6 +53,7 @@   handleBarClick   handleThumbDrag   handleChildrenFocus+  handleWheel   handleNestedWheel   handleMessageReset   raiseOnChange@@ -167,6 +168,47 @@     followNode = scroll stackNode     evtsIgnore es = nodeHandleEventEvts wenv es ignoreNode     evtsFollow es = nodeHandleEventEvts wenv es followNode++handleWheel :: Spec+handleWheel = describe "handleWheel" $ do+  it "should scroll horizontally with the horizontal wheel and click the second button" $ do+    eventsH False `shouldBe` Seq.fromList [Button2]++  it "should scroll horizontally with the horizontal wheel, ignoring shift, and click the second button" $ do+    eventsH True `shouldBe` Seq.fromList [Button2]++  it "should scroll vertically with the vertical wheel and click the third button" $ do+    eventsV False `shouldBe` Seq.fromList [Button3]++  it "should scroll horizontally with the vertical wheel, since shift is pressed, and click the second button" $ do+    eventsV True `shouldBe` Seq.fromList [Button2]++  where+    wenv shiftPressed = mockWenv ()+      & L.windowSize .~ Size 640 480+      & L.inputStatus . L.keyMod . L.leftShift .~ shiftPressed++    point = Point 160 240+    st = [width 640, height 480]+    stackNode = vstack [+        hstack [+          button "Button 1" Button1 `styleBasic` st,+          button "Button 2" Button2 `styleBasic` st+        ],+        hstack [+          button "Button 3" Button3 `styleBasic` st,+          button "Button 4" Button4 `styleBasic` st+        ]+      ]+    scrollNode = scroll stackNode++    eventsH shift = nodeHandleEventEvts (wenv shift) es scrollNode where+      evtWheel = WheelScroll point (Point (-2000) 0) WheelNormal+      es = [evtWheel, evtClick point]++    eventsV shift = nodeHandleEventEvts (wenv shift) es scrollNode where+      evtWheel = WheelScroll point (Point 0 (-2000)) WheelNormal+      es = [evtWheel, evtClick point]  handleNestedWheel :: Spec handleNestedWheel = describe "handleNestedWheel" $ do
test/unit/Monomer/Widgets/Singles/ImageSpec.hs view
@@ -36,10 +36,11 @@   it "should not create a task when merging to the same path" $     Seq.length reqs2 `shouldBe` 0 -  it "should create a task when merging to a different path" $ do-    Seq.length reqs3 `shouldBe` 2-    Seq.index reqs3 0 `shouldSatisfy` isRemoveRendererImage-    Seq.index reqs3 1 `shouldSatisfy` isRunTask+  it "should create two tasks when merging to a different path" $ do+    Seq.length reqs3 `shouldBe` 3+    Seq.index reqs3 0 `shouldSatisfy` isRunTask+    Seq.index reqs3 1 `shouldSatisfy` isRemoveRendererImage+    Seq.index reqs3 2 `shouldSatisfy` isRunTask    it "should have one widgetId on init (loading)" $     ctx1 ^. L.widgetPaths `shouldSatisfy` (== 1) . length