packages feed

monomer 1.3.0.0 → 1.4.0.0

raw patch · 107 files changed

+2694/−784 lines, 107 filesdep −HUnitdep −OpenGLdep −directoryPVP ok

version bump matches the API change (PVP)

Dependencies removed: HUnit, OpenGL, directory, safe, scientific, silently, unordered-containers

API changes (from Hackage documentation)

- Monomer.Core.Lens: instance Monomer.Core.Lens.HasTimestamp (Monomer.Core.WidgetTypes.WidgetEnv s e) Monomer.Core.WidgetTypes.Timestamp
- Monomer.Core.Lens: instance Monomer.Core.Lens.HasTs Monomer.Core.WidgetTypes.WidgetId GHC.Types.Int
- Monomer.Core.WidgetTypes: type Timestamp = Int
- Monomer.Main.Lens: class HasRenderChannel s a | s -> a
- Monomer.Main.Lens: instance Monomer.Main.Lens.HasMs Monomer.Main.Types.RenderSchedule GHC.Types.Int
- Monomer.Main.Lens: instance Monomer.Main.Lens.HasRenderChannel (Monomer.Main.Types.MonomerCtx s e) (Control.Concurrent.STM.TChan.TChan (Monomer.Main.Types.RenderMsg s e))
- Monomer.Main.Lens: instance Monomer.Main.Lens.HasStart Monomer.Main.Types.RenderSchedule GHC.Types.Int
- Monomer.Main.Lens: renderChannel :: HasRenderChannel s a => Lens' s a
- Monomer.Main.Types: [_mcRenderChannel] :: MonomerCtx s e -> TChan (RenderMsg s e)
- Monomer.Widgets.Animation.Fade: instance Monomer.Core.Combinators.CmbDuration (Monomer.Widgets.Animation.Fade.FadeCfg e) GHC.Types.Int
- Monomer.Widgets.Animation.Slide: instance Monomer.Core.Combinators.CmbDuration (Monomer.Widgets.Animation.Slide.SlideCfg e) GHC.Types.Int
- Monomer.Widgets.Composite: customModelBuilder :: CompositeCustomModelBuilder s sp -> CompositeCfg s e sp ep
- Monomer.Widgets.Composite: instance Monomer.Core.Combinators.CmbMergeRequired (Monomer.Widgets.Composite.CompositeCfg s e sp ep) s
- Monomer.Widgets.Composite: instance Monomer.Core.Combinators.CmbOnChangeReq (Monomer.Widgets.Composite.CompositeCfg s e sp ep) sp ep s
- Monomer.Widgets.Composite: instance Monomer.Core.Combinators.CmbOnDispose (Monomer.Widgets.Composite.CompositeCfg s e sp ep) e
- Monomer.Widgets.Composite: instance Monomer.Core.Combinators.CmbOnInit (Monomer.Widgets.Composite.CompositeCfg s e sp ep) e
- Monomer.Widgets.Composite: instance Monomer.Core.WidgetTypes.WidgetEvent ep => Monomer.Core.Combinators.CmbOnChange (Monomer.Widgets.Composite.CompositeCfg s e sp ep) s ep
- Monomer.Widgets.Composite: type CompositeCustomModelBuilder s sp = sp " Parent model." -> s " Old custom composite model." -> s " New custom composite model." -> s " Custom composite model."
- Monomer.Widgets.Containers.Box: instance Monomer.Core.Combinators.CmbMergeRequired (Monomer.Widgets.Containers.Box.BoxCfg s e) s
- Monomer.Widgets.Containers.Confirm: instance GHC.Classes.Eq e => GHC.Classes.Eq (Monomer.Widgets.Containers.Confirm.ConfirmEvt e)
- Monomer.Widgets.Containers.Confirm: instance GHC.Show.Show e => GHC.Show.Show (Monomer.Widgets.Containers.Confirm.ConfirmEvt e)
- Monomer.Widgets.Containers.SelectList: instance Monomer.Core.Combinators.CmbMergeRequired (Monomer.Widgets.Containers.SelectList.SelectListCfg s e a) (Data.Sequence.Internal.Seq a)
- Monomer.Widgets.Singles.DateField: instance GHC.Classes.Eq Monomer.Widgets.Singles.DateField.DateFormat
- Monomer.Widgets.Singles.DateField: instance GHC.Show.Show Monomer.Widgets.Singles.DateField.DateFormat
- Monomer.Widgets.Singles.DateField: instance Monomer.Core.Combinators.CmbCaretMs (Monomer.Widgets.Singles.DateField.DateFieldCfg s e a) GHC.Types.Int
- Monomer.Widgets.Singles.NumericField: instance Monomer.Core.Combinators.CmbCaretMs (Monomer.Widgets.Singles.NumericField.NumericFieldCfg s e a) GHC.Types.Int
- Monomer.Widgets.Singles.TextArea: instance Monomer.Core.Combinators.CmbCaretMs (Monomer.Widgets.Singles.TextArea.TextAreaCfg s e) GHC.Types.Int
- Monomer.Widgets.Singles.TextField: instance Monomer.Core.Combinators.CmbCaretMs (Monomer.Widgets.Singles.TextField.TextFieldCfg s e) GHC.Types.Int
- Monomer.Widgets.Singles.TimeField: instance GHC.Classes.Eq Monomer.Widgets.Singles.TimeField.TimeFormat
- Monomer.Widgets.Singles.TimeField: instance GHC.Show.Show Monomer.Widgets.Singles.TimeField.TimeFormat
- Monomer.Widgets.Singles.TimeField: instance Monomer.Core.Combinators.CmbCaretMs (Monomer.Widgets.Singles.TimeField.TimeFieldCfg s e a) GHC.Types.Int
- Monomer.Widgets.Util.Widget: findWidgetIdFromPath :: WidgetEnv s e -> Path -> Maybe WidgetId
+ Monomer.Common.BasicTypes: rectFromPoints :: Point -> Point -> Rect
+ Monomer.Core.Combinators: class CmbIgnoreParentEvts t
+ Monomer.Core.Combinators: class CmbOnDisposeReq t s e | t -> s e
+ Monomer.Core.Combinators: class CmbOnInitReq t s e | t -> s e
+ Monomer.Core.Combinators: class CmbReadOnly t
+ Monomer.Core.Combinators: class CmbTextLineBreak t
+ Monomer.Core.Combinators: class CmbThumbMinSize t
+ Monomer.Core.Combinators: ignoreParentEvts :: CmbIgnoreParentEvts t => t
+ Monomer.Core.Combinators: ignoreParentEvts_ :: CmbIgnoreParentEvts t => Bool -> t
+ Monomer.Core.Combinators: onDisposeReq :: CmbOnDisposeReq t s e => WidgetRequest s e -> t
+ Monomer.Core.Combinators: onInitReq :: CmbOnInitReq t s e => WidgetRequest s e -> t
+ Monomer.Core.Combinators: readOnly :: CmbReadOnly t => t
+ Monomer.Core.Combinators: readOnly_ :: CmbReadOnly t => Bool -> t
+ Monomer.Core.Combinators: styleActiveSet :: CmbStyleActive t => t -> [StyleState] -> t
+ Monomer.Core.Combinators: styleBasicSet :: CmbStyleBasic t => t -> [StyleState] -> t
+ Monomer.Core.Combinators: styleDisabledSet :: CmbStyleDisabled t => t -> [StyleState] -> t
+ Monomer.Core.Combinators: styleFocusHoverSet :: CmbStyleFocusHover t => t -> [StyleState] -> t
+ Monomer.Core.Combinators: styleFocusSet :: CmbStyleFocus t => t -> [StyleState] -> t
+ Monomer.Core.Combinators: styleHoverSet :: CmbStyleHover t => t -> [StyleState] -> t
+ Monomer.Core.Combinators: textLineBreak :: CmbTextLineBreak t => LineBreak -> t
+ Monomer.Core.Combinators: thumbMinSize :: CmbThumbMinSize t => Double -> t
+ Monomer.Core.Lens: appStartTs :: HasAppStartTs s a => Lens' s a
+ Monomer.Core.Lens: class HasAppStartTs s a | s -> a
+ Monomer.Core.Lens: class HasLineBreak s a | s -> a
+ Monomer.Core.Lens: class HasScrollThumbMinSize s a | s -> a
+ Monomer.Core.Lens: instance Monomer.Core.Lens.HasAppStartTs (Monomer.Core.WidgetTypes.WidgetEnv s e) Monomer.Core.WidgetTypes.Millisecond
+ Monomer.Core.Lens: instance Monomer.Core.Lens.HasLineBreak Monomer.Core.StyleTypes.TextStyle (GHC.Maybe.Maybe Monomer.Core.StyleTypes.LineBreak)
+ Monomer.Core.Lens: instance Monomer.Core.Lens.HasScrollThumbMinSize Monomer.Core.ThemeTypes.ThemeState GHC.Types.Double
+ Monomer.Core.Lens: instance Monomer.Core.Lens.HasTimestamp (Monomer.Core.WidgetTypes.WidgetEnv s e) Monomer.Core.WidgetTypes.Millisecond
+ Monomer.Core.Lens: instance Monomer.Core.Lens.HasTs Monomer.Core.WidgetTypes.WidgetId Monomer.Core.WidgetTypes.Millisecond
+ Monomer.Core.Lens: lineBreak :: HasLineBreak s a => Lens' s a
+ Monomer.Core.Lens: scrollThumbMinSize :: HasScrollThumbMinSize s a => Lens' s a
+ Monomer.Core.SizeReq: clearExtraH :: SizeReqUpdater
+ Monomer.Core.SizeReq: clearExtraW :: SizeReqUpdater
+ Monomer.Core.Style: instance Monomer.Core.Combinators.CmbTextLineBreak Monomer.Core.StyleTypes.StyleState
+ Monomer.Core.Style: instance Monomer.Core.Combinators.CmbTextLineBreak Monomer.Core.StyleTypes.TextStyle
+ Monomer.Core.StyleTypes: OnCharacters :: LineBreak
+ Monomer.Core.StyleTypes: OnSpaces :: LineBreak
+ Monomer.Core.StyleTypes: [_txsLineBreak] :: TextStyle -> Maybe LineBreak
+ Monomer.Core.StyleTypes: data LineBreak
+ Monomer.Core.StyleTypes: instance Data.Default.Class.Default Monomer.Core.StyleTypes.LineBreak
+ Monomer.Core.StyleTypes: instance GHC.Classes.Eq Monomer.Core.StyleTypes.LineBreak
+ Monomer.Core.StyleTypes: instance GHC.Classes.Ord Monomer.Core.StyleTypes.LineBreak
+ Monomer.Core.StyleTypes: instance GHC.Enum.Enum Monomer.Core.StyleTypes.LineBreak
+ Monomer.Core.StyleTypes: instance GHC.Generics.Generic Monomer.Core.StyleTypes.LineBreak
+ Monomer.Core.StyleTypes: instance GHC.Show.Show Monomer.Core.StyleTypes.LineBreak
+ Monomer.Core.StyleUtil: styleTextLineBreak :: StyleState -> LineBreak
+ Monomer.Core.ThemeTypes: [_thsScrollThumbMinSize] :: ThemeState -> Double
+ Monomer.Core.Util: currentTimeMs :: Integral a => WidgetEnv s e -> a
+ Monomer.Core.Util: findChildBranchByPath :: WidgetEnv s e -> WidgetNode s e -> Path -> Seq WidgetNodeInfo
+ Monomer.Core.Util: findChildNodeInfoByPath :: WidgetEnv s e -> WidgetNode s e -> Path -> Maybe WidgetNodeInfo
+ Monomer.Core.Util: findParentNodeInfoByType :: WidgetEnv s e -> Path -> WidgetType -> Maybe WidgetNodeInfo
+ Monomer.Core.Util: findWidgetIdFromPath :: WidgetEnv s e -> Path -> Maybe WidgetId
+ Monomer.Core.Util: nodeInfoFromKey :: WidgetEnv s e -> WidgetKey -> Maybe WidgetNodeInfo
+ Monomer.Core.Util: nodeInfoFromPath :: WidgetEnv s e -> Path -> Maybe WidgetNodeInfo
+ Monomer.Core.Util: widgetIdFromPath :: WidgetEnv s e -> Path -> Maybe WidgetId
+ Monomer.Core.WidgetTypes: Millisecond :: Word64 -> Millisecond
+ Monomer.Core.WidgetTypes: [_weAppStartTs] :: WidgetEnv s e -> Millisecond
+ Monomer.Core.WidgetTypes: [unMilliseconds] :: Millisecond -> Word64
+ Monomer.Core.WidgetTypes: instance Data.Default.Class.Default Monomer.Core.WidgetTypes.Millisecond
+ Monomer.Core.WidgetTypes: instance GHC.Classes.Eq Monomer.Core.WidgetTypes.Millisecond
+ Monomer.Core.WidgetTypes: instance GHC.Classes.Ord Monomer.Core.WidgetTypes.Millisecond
+ Monomer.Core.WidgetTypes: instance GHC.Enum.Bounded Monomer.Core.WidgetTypes.Millisecond
+ Monomer.Core.WidgetTypes: instance GHC.Enum.Enum Monomer.Core.WidgetTypes.Millisecond
+ Monomer.Core.WidgetTypes: instance GHC.Generics.Generic Monomer.Core.WidgetTypes.Millisecond
+ Monomer.Core.WidgetTypes: instance GHC.Num.Num Monomer.Core.WidgetTypes.Millisecond
+ Monomer.Core.WidgetTypes: instance GHC.Read.Read Monomer.Core.WidgetTypes.Millisecond
+ Monomer.Core.WidgetTypes: instance GHC.Real.Integral Monomer.Core.WidgetTypes.Millisecond
+ Monomer.Core.WidgetTypes: instance GHC.Real.Real Monomer.Core.WidgetTypes.Millisecond
+ Monomer.Core.WidgetTypes: instance GHC.Show.Show Monomer.Core.WidgetTypes.Millisecond
+ Monomer.Core.WidgetTypes: instance TextShow.Classes.TextShow Monomer.Core.WidgetTypes.Millisecond
+ Monomer.Core.WidgetTypes: newtype Millisecond
+ Monomer.Event.Keyboard: isKeyPad0 :: KeyCode -> Bool
+ Monomer.Event.Keyboard: isKeyPad1 :: KeyCode -> Bool
+ Monomer.Event.Keyboard: isKeyPad2 :: KeyCode -> Bool
+ Monomer.Event.Keyboard: isKeyPad3 :: KeyCode -> Bool
+ Monomer.Event.Keyboard: isKeyPad4 :: KeyCode -> Bool
+ Monomer.Event.Keyboard: isKeyPad5 :: KeyCode -> Bool
+ Monomer.Event.Keyboard: isKeyPad6 :: KeyCode -> Bool
+ Monomer.Event.Keyboard: isKeyPad7 :: KeyCode -> Bool
+ Monomer.Event.Keyboard: isKeyPad8 :: KeyCode -> Bool
+ Monomer.Event.Keyboard: isKeyPad9 :: KeyCode -> Bool
+ Monomer.Event.Keyboard: isKeyPadDivide :: KeyCode -> Bool
+ Monomer.Event.Keyboard: isKeyPadEnter :: KeyCode -> Bool
+ Monomer.Event.Keyboard: isKeyPadMinus :: KeyCode -> Bool
+ Monomer.Event.Keyboard: isKeyPadMultiply :: KeyCode -> Bool
+ Monomer.Event.Keyboard: isKeyPadPeriod :: KeyCode -> Bool
+ Monomer.Event.Keyboard: isKeyPadPlus :: KeyCode -> Bool
+ Monomer.Event.Keyboard: keyEnter :: KeyCode
+ Monomer.Event.Keyboard: keyPad0 :: KeyCode
+ Monomer.Event.Keyboard: keyPad1 :: KeyCode
+ Monomer.Event.Keyboard: keyPad2 :: KeyCode
+ Monomer.Event.Keyboard: keyPad3 :: KeyCode
+ Monomer.Event.Keyboard: keyPad4 :: KeyCode
+ Monomer.Event.Keyboard: keyPad5 :: KeyCode
+ Monomer.Event.Keyboard: keyPad6 :: KeyCode
+ Monomer.Event.Keyboard: keyPad7 :: KeyCode
+ Monomer.Event.Keyboard: keyPad8 :: KeyCode
+ Monomer.Event.Keyboard: keyPad9 :: KeyCode
+ Monomer.Event.Keyboard: keyPadDivide :: KeyCode
+ Monomer.Event.Keyboard: keyPadEnter :: KeyCode
+ Monomer.Event.Keyboard: keyPadMinus :: KeyCode
+ Monomer.Event.Keyboard: keyPadMultiply :: KeyCode
+ Monomer.Event.Keyboard: keyPadPeriod :: KeyCode
+ Monomer.Event.Keyboard: keyPadPlus :: KeyCode
+ Monomer.Graphics.FFI: fmSetScale :: FMContext -> Double -> IO ()
+ Monomer.Graphics.FFI: fmSetScale'_ :: FMContext -> CFloat -> IO ()
+ Monomer.Graphics.Lens: instance Monomer.Common.Lens.HasX Monomer.Graphics.Types.GlyphPos GHC.Types.Double
+ Monomer.Graphics.Types: [_glpX] :: GlyphPos -> {-# UNPACK #-} !Double
+ Monomer.Graphics.Types: [computeGlyphsPos_] :: FontManager -> Double -> Font -> FontSize -> FontSpace -> Text -> Seq GlyphPos
+ Monomer.Graphics.Types: [computeTextMetrics_] :: FontManager -> Double -> Font -> FontSize -> TextMetrics
+ Monomer.Graphics.Types: [computeTextSize_] :: FontManager -> Double -> Font -> FontSize -> FontSpace -> Text -> Size
+ Monomer.Main.Lens: class HasDisableAutoScale s a | s -> a
+ Monomer.Main.Lens: class HasRenderMethod s a | s -> a
+ Monomer.Main.Lens: class HasWindowIcon s a | s -> a
+ Monomer.Main.Lens: disableAutoScale :: HasDisableAutoScale s a => Lens' s a
+ Monomer.Main.Lens: instance Monomer.Main.Lens.HasDisableAutoScale (Monomer.Main.Types.AppConfig e) (GHC.Maybe.Maybe GHC.Types.Bool)
+ Monomer.Main.Lens: instance Monomer.Main.Lens.HasMs Monomer.Main.Types.RenderSchedule Monomer.Core.WidgetTypes.Millisecond
+ Monomer.Main.Lens: instance Monomer.Main.Lens.HasRenderMethod (Monomer.Main.Types.MonomerCtx s e) (Data.Either.Either Monomer.Graphics.Types.Renderer (Control.Concurrent.STM.TChan.TChan (Monomer.Main.Types.RenderMsg s e)))
+ Monomer.Main.Lens: instance Monomer.Main.Lens.HasStart Monomer.Main.Types.RenderSchedule Monomer.Core.WidgetTypes.Millisecond
+ Monomer.Main.Lens: instance Monomer.Main.Lens.HasWindowIcon (Monomer.Main.Types.AppConfig e) (GHC.Maybe.Maybe Data.Text.Internal.Text)
+ Monomer.Main.Lens: renderMethod :: HasRenderMethod s a => Lens' s a
+ Monomer.Main.Lens: windowIcon :: HasWindowIcon s a => Lens' s a
+ Monomer.Main.Types: MsgInit :: WidgetEnv s e -> WidgetNode s e -> RenderMsg s e
+ Monomer.Main.Types: RenderSetupMakeCurrentFailed :: String -> RenderSetupResult
+ Monomer.Main.Types: RenderSetupMulti :: RenderSetupResult
+ Monomer.Main.Types: RenderSetupSingle :: RenderSetupResult
+ Monomer.Main.Types: [_apcDisableAutoScale] :: AppConfig e -> Maybe Bool
+ Monomer.Main.Types: [_apcWindowIcon] :: AppConfig e -> Maybe Text
+ Monomer.Main.Types: [_mcRenderMethod] :: MonomerCtx s e -> Either Renderer (TChan (RenderMsg s e))
+ Monomer.Main.Types: appDisableAutoScale :: Bool -> AppConfig e
+ Monomer.Main.Types: appWindowIcon :: Text -> AppConfig e
+ Monomer.Main.Types: data RenderSetupResult
+ Monomer.Main.Types: instance GHC.Classes.Eq Monomer.Main.Types.RenderSetupResult
+ Monomer.Main.Types: instance GHC.Show.Show Monomer.Main.Types.RenderSetupResult
+ Monomer.Widgets.Animation.Fade: instance Monomer.Core.Combinators.CmbDuration (Monomer.Widgets.Animation.Fade.FadeCfg e) Monomer.Core.WidgetTypes.Millisecond
+ Monomer.Widgets.Animation.Slide: instance Monomer.Core.Combinators.CmbDuration (Monomer.Widgets.Animation.Slide.SlideCfg e) Monomer.Core.WidgetTypes.Millisecond
+ Monomer.Widgets.Composite: compositeMergeEvents :: WidgetEvent e => MergeEventsHandler s e sp -> CompositeCfg s e sp ep
+ Monomer.Widgets.Composite: compositeMergeModel :: MergeModelHandler s e sp -> CompositeCfg s e sp ep
+ Monomer.Widgets.Composite: instance Monomer.Core.Combinators.CmbMergeRequired (Monomer.Widgets.Composite.CompositeCfg s e sp ep) (Monomer.Core.WidgetTypes.WidgetEnv s e) s
+ Monomer.Widgets.Composite: instance Monomer.Core.Combinators.CmbOnChangeReq (Monomer.Widgets.Composite.CompositeCfg s e sp ep) s e s
+ Monomer.Widgets.Composite: instance Monomer.Core.Combinators.CmbOnDisposeReq (Monomer.Widgets.Composite.CompositeCfg s e sp ep) s e
+ Monomer.Widgets.Composite: instance Monomer.Core.Combinators.CmbOnInitReq (Monomer.Widgets.Composite.CompositeCfg s e sp ep) s e
+ Monomer.Widgets.Composite: instance Monomer.Core.WidgetTypes.WidgetEvent e => Monomer.Core.Combinators.CmbOnChange (Monomer.Widgets.Composite.CompositeCfg s e sp ep) s e
+ Monomer.Widgets.Composite: instance Monomer.Core.WidgetTypes.WidgetEvent e => Monomer.Core.Combinators.CmbOnDispose (Monomer.Widgets.Composite.CompositeCfg s e sp ep) e
+ Monomer.Widgets.Composite: instance Monomer.Core.WidgetTypes.WidgetEvent e => Monomer.Core.Combinators.CmbOnInit (Monomer.Widgets.Composite.CompositeCfg s e sp ep) e
+ Monomer.Widgets.Composite: type MergeEventsHandler s e sp = WidgetEnv s e " Widget environment." -> WidgetNode s e " New widget node." -> WidgetNode s e " Old widget node." -> sp " Parent model." -> s " Old composite model." -> s " New composite model." -> [e] " The list of events."
+ Monomer.Widgets.Composite: type MergeModelHandler s e sp = WidgetEnv s e " Widget environment." -> sp " Parent model." -> s " Old composite model." -> s " New composite model." -> s " Updated composite model."
+ Monomer.Widgets.Container: type ContainerCreateContainerFromModel s e a = WidgetEnv s e " The widget environment." -> WidgetNode s e " The widget node." -> a " The previous model." -> Maybe (Container s e a) " An updated Container instance."
+ Monomer.Widgets.Containers.Box: instance Monomer.Core.Combinators.CmbMergeRequired (Monomer.Widgets.Containers.Box.BoxCfg s e) (Monomer.Core.WidgetTypes.WidgetEnv s e) s
+ Monomer.Widgets.Containers.Confirm: data InnerConfirmEvt e
+ Monomer.Widgets.Containers.Confirm: instance GHC.Classes.Eq e => GHC.Classes.Eq (Monomer.Widgets.Containers.Confirm.InnerConfirmEvt e)
+ Monomer.Widgets.Containers.Confirm: instance GHC.Show.Show e => GHC.Show.Show (Monomer.Widgets.Containers.Confirm.InnerConfirmEvt e)
+ Monomer.Widgets.Containers.Keystroke: instance GHC.Classes.Eq Monomer.Widgets.Containers.Keystroke.KeyEntry
+ Monomer.Widgets.Containers.Keystroke: instance GHC.Show.Show Monomer.Widgets.Containers.Keystroke.KeyEntry
+ Monomer.Widgets.Containers.Keystroke: instance Monomer.Widgets.Containers.Keystroke.HasKsErrors Monomer.Widgets.Containers.Keystroke.KeyStroke [Data.Text.Internal.Text]
+ Monomer.Widgets.Containers.Keystroke: instance Monomer.Widgets.Containers.Keystroke.HasKsKeysText Monomer.Widgets.Containers.Keystroke.KeyStroke (Data.Set.Internal.Set Data.Text.Internal.Text)
+ Monomer.Widgets.Containers.Scroll: instance Monomer.Core.Combinators.CmbThumbMinSize (Monomer.Widgets.Containers.Scroll.ScrollCfg s e)
+ Monomer.Widgets.Containers.SelectList: instance Monomer.Core.Combinators.CmbMergeRequired (Monomer.Widgets.Containers.SelectList.SelectListCfg s e a) (Monomer.Core.WidgetTypes.WidgetEnv s e) (Data.Sequence.Internal.Seq a)
+ Monomer.Widgets.Singles.Base.InputField: [_ifcReadOnly] :: InputFieldCfg s e a -> Bool
+ Monomer.Widgets.Singles.Button: buttonD_ :: WidgetEvent e => Text -> [ButtonCfg s e] -> WidgetNode s e
+ Monomer.Widgets.Singles.Button: instance Monomer.Core.Combinators.CmbIgnoreParentEvts (Monomer.Widgets.Singles.Button.ButtonCfg s e)
+ Monomer.Widgets.Singles.Button: mainButtonD_ :: WidgetEvent e => Text -> [ButtonCfg s e] -> WidgetNode s e
+ Monomer.Widgets.Singles.ColorPicker: data ColorPickerEvt
+ Monomer.Widgets.Singles.DateField: data DateFieldFormat
+ Monomer.Widgets.Singles.DateField: dateAcceptText :: DateTextConverter a => DateFieldFormat -> Char -> Maybe a -> Maybe a -> Text -> (Bool, Bool, Maybe a)
+ Monomer.Widgets.Singles.DateField: dateFieldD_ :: (FormattableDate a, WidgetEvent e) => WidgetData s a -> [DateFieldCfg s e a] -> WidgetNode s e
+ Monomer.Widgets.Singles.DateField: dateFromDay :: DateTextConverter a => Day -> a
+ Monomer.Widgets.Singles.DateField: dateFromText :: DateTextConverter a => DateFieldFormat -> Char -> Text -> Maybe a
+ Monomer.Widgets.Singles.DateField: dateToDay :: DateTextConverter a => a -> Maybe Day
+ Monomer.Widgets.Singles.DateField: dateToText :: DateTextConverter a => DateFieldFormat -> Char -> a -> Text
+ Monomer.Widgets.Singles.DateField: instance GHC.Classes.Eq Monomer.Widgets.Singles.DateField.DateFieldFormat
+ Monomer.Widgets.Singles.DateField: instance GHC.Show.Show Monomer.Widgets.Singles.DateField.DateFieldFormat
+ Monomer.Widgets.Singles.DateField: instance Monomer.Core.Combinators.CmbCaretMs (Monomer.Widgets.Singles.DateField.DateFieldCfg s e a) Monomer.Core.WidgetTypes.Millisecond
+ Monomer.Widgets.Singles.DateField: instance Monomer.Core.Combinators.CmbReadOnly (Monomer.Widgets.Singles.DateField.DateFieldCfg s e a)
+ Monomer.Widgets.Singles.NumericField: instance Monomer.Core.Combinators.CmbCaretMs (Monomer.Widgets.Singles.NumericField.NumericFieldCfg s e a) Monomer.Core.WidgetTypes.Millisecond
+ Monomer.Widgets.Singles.NumericField: instance Monomer.Core.Combinators.CmbReadOnly (Monomer.Widgets.Singles.NumericField.NumericFieldCfg s e a)
+ Monomer.Widgets.Singles.NumericField: numericAcceptText :: NumericTextConverter a => Maybe a -> Maybe a -> Int -> Text -> (Bool, Bool, Maybe a)
+ Monomer.Widgets.Singles.NumericField: numericFieldD_ :: forall s e a. (FormattableNumber a, WidgetEvent e) => WidgetData s a -> [NumericFieldCfg s e a] -> WidgetNode s e
+ Monomer.Widgets.Singles.NumericField: numericFromFractional :: (NumericTextConverter a, Real b, Fractional b) => b -> a
+ Monomer.Widgets.Singles.NumericField: numericFromText :: NumericTextConverter a => Text -> Maybe a
+ Monomer.Widgets.Singles.NumericField: numericToFractional :: (NumericTextConverter a, Fractional b) => a -> Maybe b
+ Monomer.Widgets.Singles.NumericField: numericToText :: NumericTextConverter a => Int -> a -> Text
+ Monomer.Widgets.Singles.OptionButton: instance Monomer.Core.Combinators.CmbOnClickReq (Monomer.Widgets.Singles.OptionButton.OptionButtonCfg s e a) s e
+ Monomer.Widgets.Singles.OptionButton: instance Monomer.Core.WidgetTypes.WidgetEvent e => Monomer.Core.Combinators.CmbOnClick (Monomer.Widgets.Singles.OptionButton.OptionButtonCfg s e a) e
+ Monomer.Widgets.Singles.Radio: instance Monomer.Core.Combinators.CmbOnClickReq (Monomer.Widgets.Singles.Radio.RadioCfg s e a) s e
+ Monomer.Widgets.Singles.Radio: instance Monomer.Core.WidgetTypes.WidgetEvent e => Monomer.Core.Combinators.CmbOnClick (Monomer.Widgets.Singles.Radio.RadioCfg s e a) e
+ Monomer.Widgets.Singles.TextArea: instance Monomer.Core.Combinators.CmbCaretMs (Monomer.Widgets.Singles.TextArea.TextAreaCfg s e) Monomer.Core.WidgetTypes.Millisecond
+ Monomer.Widgets.Singles.TextArea: instance Monomer.Core.Combinators.CmbReadOnly (Monomer.Widgets.Singles.TextArea.TextAreaCfg s e)
+ Monomer.Widgets.Singles.TextArea: textAreaD_ :: WidgetEvent e => WidgetData s Text -> [TextAreaCfg s e] -> WidgetNode s e
+ Monomer.Widgets.Singles.TextField: instance Monomer.Core.Combinators.CmbCaretMs (Monomer.Widgets.Singles.TextField.TextFieldCfg s e) Monomer.Core.WidgetTypes.Millisecond
+ Monomer.Widgets.Singles.TextField: instance Monomer.Core.Combinators.CmbReadOnly (Monomer.Widgets.Singles.TextField.TextFieldCfg s e)
+ Monomer.Widgets.Singles.TimeField: class TimeTextConverter a
+ Monomer.Widgets.Singles.TimeField: data TimeFieldFormat
+ Monomer.Widgets.Singles.TimeField: instance GHC.Classes.Eq Monomer.Widgets.Singles.TimeField.TimeFieldFormat
+ Monomer.Widgets.Singles.TimeField: instance GHC.Show.Show Monomer.Widgets.Singles.TimeField.TimeFieldFormat
+ Monomer.Widgets.Singles.TimeField: instance Monomer.Core.Combinators.CmbCaretMs (Monomer.Widgets.Singles.TimeField.TimeFieldCfg s e a) Monomer.Core.WidgetTypes.Millisecond
+ Monomer.Widgets.Singles.TimeField: instance Monomer.Core.Combinators.CmbReadOnly (Monomer.Widgets.Singles.TimeField.TimeFieldCfg s e a)
+ Monomer.Widgets.Singles.TimeField: timeAcceptText :: TimeTextConverter a => TimeFieldFormat -> Maybe a -> Maybe a -> Text -> (Bool, Bool, Maybe a)
+ Monomer.Widgets.Singles.TimeField: timeFieldD_ :: (FormattableTime a, WidgetEvent e) => WidgetData s a -> [TimeFieldCfg s e a] -> WidgetNode s e
+ Monomer.Widgets.Singles.TimeField: timeFromText :: TimeTextConverter a => TimeFieldFormat -> Text -> Maybe a
+ Monomer.Widgets.Singles.TimeField: timeFromTimeOfDay' :: TimeTextConverter a => TimeOfDay -> a
+ Monomer.Widgets.Singles.TimeField: timeToText :: TimeTextConverter a => TimeFieldFormat -> a -> Text
+ Monomer.Widgets.Singles.TimeField: timeToTimeOfDay' :: TimeTextConverter a => a -> Maybe TimeOfDay
- Monomer.Core.Combinators: class CmbMergeRequired t s | t -> s
+ Monomer.Core.Combinators: class CmbMergeRequired t w s | t -> w s
- Monomer.Core.Combinators: mergeRequired :: CmbMergeRequired t s => (s -> s -> Bool) -> t
+ Monomer.Core.Combinators: mergeRequired :: CmbMergeRequired t w s => (w -> s -> s -> Bool) -> t
- Monomer.Core.Lens: _WidgetLens :: forall s_a1J2b a_a1xuz s_a1xuy. Prism (WidgetData s_a1J2b a_a1xuz) (WidgetData s_a1xuy a_a1xuz) (ALens' s_a1J2b a_a1xuz) (ALens' s_a1xuy a_a1xuz)
+ Monomer.Core.Lens: _WidgetLens :: forall s_a1NwM a_a1ART s_a1ARS. Prism (WidgetData s_a1NwM a_a1ART) (WidgetData s_a1ARS a_a1ART) (ALens' s_a1NwM a_a1ART) (ALens' s_a1ARS a_a1ART)
- Monomer.Core.Lens: _WidgetValue :: forall s_a1xuy a_a1xuz. Prism' (WidgetData s_a1xuy a_a1xuz) a_a1xuz
+ Monomer.Core.Lens: _WidgetValue :: forall s_a1ARS a_a1ART. Prism' (WidgetData s_a1ARS a_a1ART) a_a1ART
- Monomer.Core.StyleTypes: TextStyle :: Maybe Font -> Maybe FontSize -> Maybe FontSpace -> Maybe FontSpace -> Maybe Color -> Maybe Bool -> Maybe Bool -> Maybe Bool -> Maybe AlignTH -> Maybe AlignTV -> TextStyle
+ Monomer.Core.StyleTypes: TextStyle :: Maybe Font -> Maybe FontSize -> Maybe FontSpace -> Maybe FontSpace -> Maybe Color -> Maybe Bool -> Maybe Bool -> Maybe Bool -> Maybe AlignTH -> Maybe AlignTV -> Maybe LineBreak -> TextStyle
- Monomer.Core.ThemeTypes: ThemeState :: StyleState -> StyleState -> StyleState -> StyleState -> Double -> StyleState -> StyleState -> Rational -> Double -> StyleState -> StyleState -> StyleState -> StyleState -> StyleState -> Double -> StyleState -> StyleState -> StyleState -> StyleState -> StyleState -> StyleState -> StyleState -> StyleState -> StyleState -> StyleState -> Double -> Bool -> Bool -> Color -> Color -> Double -> Double -> Double -> Rational -> Double -> StyleState -> StyleState -> StyleState -> StyleState -> StyleState -> Maybe Double -> Double -> Rational -> Double -> StyleState -> StyleState -> StyleState -> StyleState -> StyleState -> StyleState -> Map String StyleState -> Map String Color -> ThemeState
+ Monomer.Core.ThemeTypes: ThemeState :: StyleState -> StyleState -> StyleState -> StyleState -> Double -> StyleState -> StyleState -> Rational -> Double -> StyleState -> StyleState -> StyleState -> StyleState -> StyleState -> Double -> StyleState -> StyleState -> StyleState -> StyleState -> StyleState -> StyleState -> StyleState -> StyleState -> StyleState -> StyleState -> Double -> Bool -> Bool -> Color -> Color -> Double -> Double -> Double -> Double -> Rational -> Double -> StyleState -> StyleState -> StyleState -> StyleState -> StyleState -> Maybe Double -> Double -> Rational -> Double -> StyleState -> StyleState -> StyleState -> StyleState -> StyleState -> StyleState -> Map String StyleState -> Map String Color -> ThemeState
- Monomer.Core.WidgetTypes: RenderEvery :: WidgetId -> Int -> Maybe Int -> WidgetRequest s e
+ Monomer.Core.WidgetTypes: RenderEvery :: WidgetId -> Millisecond -> Maybe Int -> WidgetRequest s e
- Monomer.Core.WidgetTypes: WidgetEnv :: Text -> Double -> FontManager -> (Path -> Seq WidgetNodeInfo) -> Button -> Button -> Theme -> Size -> MVar (Map Text WidgetShared) -> WidgetKeyMap s e -> Maybe Path -> Path -> Maybe Path -> Maybe (Path, WidgetDragMsg) -> Maybe (Path, Point) -> Maybe (Path, CursorIcon) -> s -> InputStatus -> Timestamp -> Bool -> (Point -> Bool) -> LayoutDirection -> Rect -> Point -> WidgetEnv s e
+ Monomer.Core.WidgetTypes: WidgetEnv :: Text -> Double -> Millisecond -> FontManager -> (Path -> Seq WidgetNodeInfo) -> Button -> Button -> Theme -> Size -> MVar (Map Text WidgetShared) -> WidgetKeyMap s e -> Maybe Path -> Path -> Maybe Path -> Maybe (Path, WidgetDragMsg) -> Maybe (Path, Point) -> Maybe (Path, CursorIcon) -> s -> InputStatus -> Millisecond -> Bool -> (Point -> Bool) -> LayoutDirection -> Rect -> Point -> WidgetEnv s e
- Monomer.Core.WidgetTypes: WidgetId :: Int -> Path -> WidgetId
+ Monomer.Core.WidgetTypes: WidgetId :: Millisecond -> Path -> WidgetId
- Monomer.Core.WidgetTypes: [_weTimestamp] :: WidgetEnv s e -> Timestamp
+ Monomer.Core.WidgetTypes: [_weTimestamp] :: WidgetEnv s e -> Millisecond
- Monomer.Core.WidgetTypes: [_widTs] :: WidgetId -> Int
+ Monomer.Core.WidgetTypes: [_widTs] :: WidgetId -> Millisecond
- Monomer.Graphics.Types: FontManager :: (Font -> FontSize -> TextMetrics) -> (Font -> FontSize -> FontSpace -> Text -> Size) -> (Font -> FontSize -> FontSpace -> Text -> Seq GlyphPos) -> FontManager
+ Monomer.Graphics.Types: FontManager :: (Font -> FontSize -> TextMetrics) -> (Double -> Font -> FontSize -> TextMetrics) -> (Font -> FontSize -> FontSpace -> Text -> Size) -> (Double -> Font -> FontSize -> FontSpace -> Text -> Size) -> (Font -> FontSize -> FontSpace -> Text -> Seq GlyphPos) -> (Double -> Font -> FontSize -> FontSpace -> Text -> Seq GlyphPos) -> FontManager
- Monomer.Graphics.Types: GlyphPos :: {-# UNPACK #-} !Char -> {-# UNPACK #-} !Double -> {-# UNPACK #-} !Double -> {-# UNPACK #-} !Double -> {-# UNPACK #-} !Double -> {-# UNPACK #-} !Double -> {-# UNPACK #-} !Double -> GlyphPos
+ Monomer.Graphics.Types: GlyphPos :: {-# UNPACK #-} !Char -> {-# UNPACK #-} !Double -> {-# UNPACK #-} !Double -> {-# UNPACK #-} !Double -> {-# UNPACK #-} !Double -> {-# UNPACK #-} !Double -> {-# UNPACK #-} !Double -> {-# UNPACK #-} !Double -> GlyphPos
- Monomer.Main.Types: AppConfig :: Maybe MainWindowState -> Maybe Text -> Maybe Bool -> Maybe Bool -> Maybe Bool -> Maybe Int -> Maybe Double -> [FontDef] -> Maybe Theme -> [e] -> [e] -> [e] -> [Rect -> e] -> Maybe Button -> Maybe Button -> Maybe Bool -> Maybe Bool -> Maybe Bool -> AppConfig e
+ Monomer.Main.Types: AppConfig :: Maybe MainWindowState -> Maybe Text -> Maybe Bool -> Maybe Bool -> Maybe Text -> Maybe Bool -> Maybe Int -> Maybe Double -> Maybe Bool -> [FontDef] -> Maybe Theme -> [e] -> [e] -> [e] -> [Rect -> e] -> Maybe Button -> Maybe Button -> Maybe Bool -> Maybe Bool -> Maybe Bool -> AppConfig e
- Monomer.Main.Types: MonomerCtx :: s -> ~Window -> Size -> Double -> Double -> TChan (RenderMsg s e) -> InputStatus -> [(WidgetId, CursorIcon)] -> WidgetId -> Maybe WidgetId -> Maybe WidgetId -> Maybe DragAction -> Maybe (Path, Point) -> Seq WidgetTask -> Map WidgetId Path -> Map CursorIcon Cursor -> Bool -> Seq WidgetId -> Bool -> Map WidgetId RenderSchedule -> Bool -> MonomerCtx s e
+ Monomer.Main.Types: MonomerCtx :: s -> ~Window -> Size -> Double -> Double -> Either Renderer (TChan (RenderMsg s e)) -> InputStatus -> [(WidgetId, CursorIcon)] -> WidgetId -> Maybe WidgetId -> Maybe WidgetId -> Maybe DragAction -> Maybe (Path, Point) -> Seq WidgetTask -> Map WidgetId Path -> Map CursorIcon Cursor -> Bool -> Seq WidgetId -> Bool -> Map WidgetId RenderSchedule -> Bool -> MonomerCtx s e
- Monomer.Main.Types: RenderSchedule :: WidgetId -> Int -> Int -> Maybe Int -> RenderSchedule
+ Monomer.Main.Types: RenderSchedule :: WidgetId -> Millisecond -> Millisecond -> Maybe Int -> RenderSchedule
- Monomer.Main.Types: [_rsMs] :: RenderSchedule -> Int
+ Monomer.Main.Types: [_rsMs] :: RenderSchedule -> Millisecond
- Monomer.Main.Types: [_rsStart] :: RenderSchedule -> Int
+ Monomer.Main.Types: [_rsStart] :: RenderSchedule -> Millisecond
- Monomer.Widgets.Composite: compositeMergeReqs :: MergeReqsHandler s e -> CompositeCfg s e sp ep
+ Monomer.Widgets.Composite: compositeMergeReqs :: MergeReqsHandler s e sp -> CompositeCfg s e sp ep
- Monomer.Widgets.Composite: compositeV :: (CompositeModel s, CompositeEvent e, CompositeEvent ep, CompParentModel sp) => WidgetType -> s -> (s -> ep) -> UIBuilder s e -> EventHandler s e sp ep -> WidgetNode sp ep
+ Monomer.Widgets.Composite: compositeV :: (CompositeModel s, CompositeEvent e, CompositeEvent ep, CompParentModel sp) => WidgetType -> s -> (s -> e) -> UIBuilder s e -> EventHandler s e sp ep -> WidgetNode sp ep
- Monomer.Widgets.Composite: compositeV_ :: (CompositeModel s, CompositeEvent e, CompositeEvent ep, CompParentModel sp) => WidgetType -> s -> (s -> ep) -> UIBuilder s e -> EventHandler s e sp ep -> [CompositeCfg s e sp ep] -> WidgetNode sp ep
+ Monomer.Widgets.Composite: compositeV_ :: (CompositeModel s, CompositeEvent e, CompositeEvent ep, CompParentModel sp) => WidgetType -> s -> (s -> e) -> UIBuilder s e -> EventHandler s e sp ep -> [CompositeCfg s e sp ep] -> WidgetNode sp ep
- Monomer.Widgets.Composite: type MergeReqsHandler s e = WidgetEnv s e " Widget environment." -> WidgetNode s e " New widget node." -> WidgetNode s e " Old widget node." -> s " The current model." -> [WidgetRequest s e] " The list of requests."
+ Monomer.Widgets.Composite: type MergeReqsHandler s e sp = WidgetEnv s e " Widget environment." -> WidgetNode s e " New widget node." -> WidgetNode s e " Old widget node." -> sp " Parent model." -> s " Old composite model." -> s " New composite model." -> [WidgetRequest s e] " The list of requests."
- Monomer.Widgets.Composite: type MergeRequired s = s " Old composite model." -> s " New composite model" -> Bool " True if merge is required."
+ Monomer.Widgets.Composite: type MergeRequired s e = WidgetEnv s e " Widget environment." -> s " Old composite model." -> s " New composite model" -> Bool " True if merge is required."
- Monomer.Widgets.Containers.Confirm: confirm :: (WidgetModel s, WidgetEvent e) => e -> e -> WidgetNode () (ConfirmEvt e) -> WidgetNode s e
+ Monomer.Widgets.Containers.Confirm: confirm :: (WidgetModel s, WidgetEvent e) => e -> e -> WidgetNode () (InnerConfirmEvt e) -> WidgetNode s e
- Monomer.Widgets.Containers.Confirm: confirm_ :: (WidgetModel s, WidgetEvent e) => e -> e -> [ConfirmCfg] -> WidgetNode () (ConfirmEvt e) -> WidgetNode s e
+ Monomer.Widgets.Containers.Confirm: confirm_ :: (WidgetModel s, WidgetEvent e) => e -> e -> [ConfirmCfg] -> WidgetNode () (InnerConfirmEvt e) -> WidgetNode s e
- Monomer.Widgets.Containers.Tooltip: tooltipDelay :: Int -> TooltipCfg
+ Monomer.Widgets.Containers.Tooltip: tooltipDelay :: Millisecond -> TooltipCfg
- Monomer.Widgets.Singles.Base.InputField: InputFieldCfg :: Maybe Text -> a -> WidgetData s a -> Maybe (WidgetData s Bool) -> [Bool -> e] -> Bool -> Double -> Maybe Double -> Maybe Int -> Maybe Char -> Bool -> Bool -> (Text -> Maybe a) -> (a -> Text) -> (Text -> Bool) -> (Text -> Bool) -> Maybe (ALens' ThemeState StyleState) -> Maybe (InputWheelHandler a) -> Maybe (InputDragHandler a) -> Maybe CursorIcon -> [Path -> WidgetRequest s e] -> [Path -> WidgetRequest s e] -> [a -> WidgetRequest s e] -> InputFieldCfg s e a
+ Monomer.Widgets.Singles.Base.InputField: InputFieldCfg :: Maybe Text -> a -> WidgetData s a -> Maybe (WidgetData s Bool) -> [Bool -> e] -> Bool -> Double -> Maybe Double -> Maybe Millisecond -> Maybe Char -> Bool -> Bool -> Bool -> (Text -> Maybe a) -> (a -> Text) -> (Text -> Bool) -> (Text -> Bool) -> Maybe (ALens' ThemeState StyleState) -> Maybe (InputWheelHandler a) -> Maybe (InputDragHandler a) -> Maybe CursorIcon -> [Path -> WidgetRequest s e] -> [Path -> WidgetRequest s e] -> [a -> WidgetRequest s e] -> InputFieldCfg s e a
- Monomer.Widgets.Singles.Base.InputField: InputFieldState :: Seq TextLine -> a -> !Text -> !Int -> Maybe Int -> a -> Seq GlyphPos -> !Double -> Rect -> TextMetrics -> Seq (HistoryStep a) -> Int -> Int -> InputFieldState a
+ Monomer.Widgets.Singles.Base.InputField: InputFieldState :: Seq TextLine -> a -> !Text -> !Int -> Maybe Int -> a -> Seq GlyphPos -> !Double -> Rect -> TextMetrics -> Seq (HistoryStep a) -> Int -> Millisecond -> InputFieldState a
- Monomer.Widgets.Singles.Base.InputField: [_ifcCaretMs] :: InputFieldCfg s e a -> Maybe Int
+ Monomer.Widgets.Singles.Base.InputField: [_ifcCaretMs] :: InputFieldCfg s e a -> Maybe Millisecond
- Monomer.Widgets.Singles.Base.InputField: [_ifsFocusStart] :: InputFieldState a -> Int
+ Monomer.Widgets.Singles.Base.InputField: [_ifsFocusStart] :: InputFieldState a -> Millisecond
- Monomer.Widgets.Util.Widget: delayedMessage :: Typeable i => WidgetNode s e -> i -> Int -> WidgetRequest s e
+ Monomer.Widgets.Util.Widget: delayedMessage :: Typeable i => WidgetNode s e -> i -> Millisecond -> WidgetRequest s e
- Monomer.Widgets.Util.Widget: delayedMessage_ :: Typeable i => WidgetId -> Path -> i -> Int -> WidgetRequest s e
+ Monomer.Widgets.Util.Widget: delayedMessage_ :: Typeable i => WidgetId -> Path -> i -> Millisecond -> WidgetRequest s e

Files

ChangeLog.md view
@@ -1,3 +1,64 @@+## 1.4.0.0++### Breaking changes++- Added `style...Set` family of functions ([PR #104](https://github.com/fjvallarino/monomer/pull/104)).+- `Composite`'s `onChange` event is now sent to its `handleEvent` function, not to its parent; the type of the+  generated event was updated to reflect this change. The rationale is that since `onInit` is sent to+  `handleEvent`, having `onChange` sent to its parent was confusing. At the same time there was not an easy way+  in `handleEvent` to know when the model changed. Widgets that want to report model changes to its parent can+  use `Report`/`RequestParent`; an example can be found in `ColorPicker` ([PR #71](https://github.com/fjvallarino/monomer/pull/71)).+- `Timestamp` is now a newtype. Enforce use of this type instead of `Int` when appropriate ([PR #103](https://github.com/fjvallarino/monomer/pull/103)).+- `Timestamp` was renamed to `Millisecond`. The rationale is that since both timestamps and durations are used frequently in calculations (and in the context of Monomer timestamps and durations indeed represent time in milliseconds), having separate types for Timestamp and Duration caused more harm than good ([PR #107](https://github.com/fjvallarino/monomer/pull/107)).+- `compositeMergeModel` (previously `customModelBuilder`) now receives `WidgetEnv` as its first parameter ([PR #114](https://github.com/fjvallarino/monomer/pull/114)).+- `compositeMergeReqs` now receives `parentModel` and `oldModel` too ([PR #114](https://github.com/fjvallarino/monomer/pull/114)).+- `mergeRequired` now receives an extra value as its first parameter, usually `WidgetEnv` ([PR #122](https://github.com/fjvallarino/monomer/pull/122)).++### Fixed++- Properly handle `SetFocusOnKey` for `textArea` ([#80](https://github.com/fjvallarino/monomer/issues/80)).+- Lens tutorial sample code ([PR #95](https://github.com/fjvallarino/monomer/pull/95) and [PR #98](https://github.com/fjvallarino/monomer/pull/98)). Thanks @Clindbergh!+- ColorPicker's numericFields vertical alignment ([PR #108](https://github.com/fjvallarino/monomer/pull/108)).+- Differences in glyphs positions used by `FontManager` and nanovg; temporary workaround ([PR #105](https://github.com/fjvallarino/monomer/pull/105)).+- `nodeInfoFromKey` relies on `nodeInfoFromPath` to retrieve information instead of fetching it directly from `WidgetEnv`'s `widgetKeyMap`, which can be stale ([PR #110](https://github.com/fjvallarino/monomer/pull/110)).+- Glyph positioning issues in `FontManager`; removed workaround added in #105 ([PR #125](https://github.com/fjvallarino/monomer/pull/125)).+- Will attempt to fall back to rendering on the main thread if threaded rendering setup fails ([PR #131](https://github.com/fjvallarino/monomer/pull/131)).+- Space leak in StyleUtil's mergeNodeStyleState ([PR #132](https://github.com/fjvallarino/monomer/pull/132)).++### Added++- Utility functions `rectFromPoints`, `nodeInfoFromKey`, `nodeInfoFromPath` and `findParentNodeInfoByType`.+- Allow setting the window icon via AppConfig ([PR #79](https://github.com/fjvallarino/monomer/pull/79)). Thanks @Dretch!+- Support for breaking text lines at character boundaries ([PR #86](https://github.com/fjvallarino/monomer/pull/86)). Thanks @toku-sa-n!+- Read-only mode for `textField`, `numericField`, `dateField`, `timeField` and `textArea` ([PR #93](https://github.com/fjvallarino/monomer/pull/93)). Thanks @Dretch!+- The `scroll` widget now supports a `thumbMinSize` configuration option that allows setting a minimum thumb size ([PR #100](https://github.com/fjvallarino/monomer/pull/100)).+- New field `_weAppStartTs` in `WidgetEnv`, complementary to `_weTimestamp`, representing the time in milliseconds when the application started. Added utility function `currentTimeMs` that returns their sum with a polymorphic type ([PR #103](https://github.com/fjvallarino/monomer/pull/103)).+- Several sizeReq helpers ([PR #106](https://github.com/fjvallarino/monomer/pull/106)).+- `compositeMergeEvents`, for completeness ([PR #114](https://github.com/fjvallarino/monomer/pull/114)).+- Support for symbols and other keys in `keystroke` ([PR #117](https://github.com/fjvallarino/monomer/pull/117)).+- New constructor (`buttonD_`) and `ignoreParentEvts` configuration option to `button` ([PR #123](https://github.com/fjvallarino/monomer/pull/123)).+- Allow disabling auto scale detection with `appDisableAutoScale` ([PR #128](https://github.com/fjvallarino/monomer/pull/128)).++### Changed++- The `keystroke` widget now supports the `Backspace` key ([PR #74](https://github.com/fjvallarino/monomer/pull/74)).+- `style...` family of functions now combine new attributes with the existing ones ([PR #104](https://github.com/fjvallarino/monomer/pull/104)).+- `radio` and `optionButton` now only trigger `onChange` when their value changes. `onClick` was can be used to replicate the previous `onChange` behavior ([PR #134](https://github.com/fjvallarino/monomer/pull/134)).++### Renamed++- Utility functions for retrieving `WidgetNode` information ([PR #75](https://github.com/fjvallarino/monomer/pull/75))+  - `findWidgetByPath` -> `findChildNodeInfoByPath`.+  - `findWidgetBranchByPath` -> `findChildBranchByPath`.+  - `findWidgetIdFromPath` -> `widgetIdFromPath`.+- Composite merge related ([PR #114](https://github.com/fjvallarino/monomer/pull/114))+  - `customModelBuilder` -> `compositeMergeModel`+  - `CompositeCustomModelBuilder` -> `MergeModelHandler`.++### Removed++- Dependencies on `OpenGL`, `Safe`, `scientific`, `unordered-containers`, `directory`, `HUnit` and `silently` ([PR #70](https://github.com/fjvallarino/monomer/pull/70)).+ ## 1.3.0.0  ### Added
cbits/fontmanager.c view
@@ -37,6 +37,7 @@ 	// Initialize font manager context 	ctx->fs = fonsCreateInternal(&fontParams); 	ctx->dpr = dpr;+	ctx->scale = 1; 	ctx->fontSize = 16.0f; 	ctx->letterSpacing = 0.0f; 	ctx->lineHeight = 1.0f;@@ -52,6 +53,10 @@ 	return fonsAddFont(ctx->fs, name, filename, 0); } +void fmSetScale(FMcontext* ctx, float scale) {+	ctx->scale = scale;+}+ void fmFontFace(FMcontext* ctx, const char* font) { 	ctx->fontId = fonsGetFontByName(ctx->fs, font);@@ -79,7 +84,7 @@  void fmTextMetrics(FMcontext* ctx, float* ascender, float* descender, float* lineh) {-	float scale = ctx->dpr;+	float scale = ctx->dpr * ctx->scale; 	float invscale = 1.0f / scale;  	if (ctx->fontId == FONS_INVALID) return;@@ -101,7 +106,7 @@  float fmTextBounds(FMcontext* ctx, float x, float y, const char* string, const char* end, float* bounds) {-	float scale = ctx->dpr;+	float scale = ctx->dpr * ctx->scale; 	float invscale = 1.0f / scale; 	float width; @@ -127,7 +132,7 @@  int fmTextGlyphPositions(FMcontext* ctx, float x, float y, const char* string, const char* end, FMGglyphPosition* positions, int maxPositions) {-	float scale = ctx->dpr;+	float scale = ctx->dpr * ctx->scale; 	float invscale = 1.0f / scale; 	FONStextIter iter, prevIter; 	FONSquad q;
cbits/fontmanager.h view
@@ -15,6 +15,7 @@ struct FMcontext { 	struct FONScontext* fs; 	float dpr;+	float scale; 	float fontSize; 	float letterSpacing; 	float lineHeight;@@ -37,6 +38,8 @@ FMcontext* fmInit(float dpr);  int fmCreateFont(FMcontext* ctx, const char* name, const char* filename);++void fmSetScale(FMcontext* ctx, float scale);  void fmFontFace(FMcontext* ctx, const char* font); 
examples/books/Main.hs view
@@ -119,7 +119,7 @@   countLabel = label caption `styleBasic` [padding 10] where     caption = "Books (" <> showt (length $ model ^. books) <> ")" -  booksChanged old new = old ^. books /= new ^. books+  booksChanged wenv old new = old ^. books /= new ^. books    widgetTree = zstack [       vstack [@@ -190,6 +190,7 @@   where     config = [       appWindowTitle "Book search",+      appWindowIcon "./assets/images/icon.bmp",       appTheme customDarkTheme,       appFontDef "Regular" "./assets/fonts/Roboto-Regular.ttf",       appFontDef "Medium" "./assets/fonts/Roboto-Medium.ttf",
examples/generative/Main.hs view
@@ -112,6 +112,7 @@     model = GenerativeModel CirclesGrid False def def     config = [       appWindowTitle "Generative",+      appWindowIcon "./assets/images/icon.bmp",       appTheme darkTheme,       appFontDef "Remix" "./assets/fonts/remixicon.ttf",       appFontDef "Regular" "./assets/fonts/Roboto-Regular.ttf",
examples/opengl/Main.hs view
@@ -94,6 +94,7 @@   where     config = [       appWindowTitle "OpenGL",+      appWindowIcon "./assets/images/icon.bmp",       appTheme darkTheme,       appFontDef "Regular" "./assets/fonts/Roboto-Regular.ttf",       appInitEvent AppInit
examples/ticker/BinanceTypes.hs view
@@ -22,12 +22,14 @@ import Control.Lens.TH import Data.Aeson import Data.Default+import Data.Fixed import Data.Foldable (asum) import Data.Maybe import Data.Map (Map)-import Data.Scientific import Data.Text (Text, pack) +type FixedFloat = Pico+ data ServerRequest = ServerRequest {   _srqRequestId :: Int,   _srqMethod :: Text,@@ -68,12 +70,12 @@ data Ticker = Ticker {   _tckSymbolPair :: Text,   _tckTs :: Int,-  _tckOpen :: Scientific,-  _tckClose :: Scientific,-  _tckHigh :: Scientific,-  _tckLow :: Scientific,-  _tckVolume :: Scientific,-  _tckTrades :: Scientific+  _tckOpen :: FixedFloat,+  _tckClose :: FixedFloat,+  _tckHigh :: FixedFloat,+  _tckLow :: FixedFloat,+  _tckVolume :: FixedFloat,+  _tckTrades :: FixedFloat } deriving (Eq, Show)  instance FromJSON Ticker where
examples/ticker/Main.hs view
@@ -25,9 +25,9 @@ import Data.Default import Data.Foldable (foldl') import Data.Maybe-import Data.Scientific import Data.Text (Text) +import qualified Formatting as F import qualified Data.Map as M import qualified Data.Text as T import qualified Network.Wreq as W@@ -46,10 +46,10 @@  tickerPct :: Ticker -> TickerNode tickerPct t = pctLabel where-  diff = toRealFloat $ 100 * (t ^. close - t ^. open)-  pct = diff / toRealFloat (t ^. open)+  diff = 100 * (t ^. close - t ^. open)+  pct = diff / t ^. open -  pctText = formatTickerPct (fromFloatDigits pct) <> "%"+  pctText = formatTickerPct pct <> "%"   pctColor     | abs pct < 0.01 = rgbHex "#428FE0"     | pct > 0 = rgbHex "#51A39A"@@ -170,7 +170,7 @@  handleSubscription :: AppEnv -> [Text] -> Text -> IO TickerEvt handleSubscription env pairs action = do-  liftIO . atomically $ writeTChan (env^.channel) req+  atomically $ writeTChan (env ^. channel) req   return TickerIgnore   where     subscription pair = T.toLower pair <> "@miniTicker"@@ -213,8 +213,8 @@  groupTickers :: TChan Ticker -> (TickerEvt -> IO a) -> IO () groupTickers channel sendMsg = void . forkIO . forever $ do-  ticker <- liftIO . atomically $ readTChan channel-  tickers <- collectJustM . liftIO . atomically $ tryReadTChan channel+  ticker <- atomically $ readTChan channel+  tickers <- collectJustM . atomically $ tryReadTChan channel   sendMsg $ TickerUpdate (ticker : tickers)    threadDelay $ 500 * 1000@@ -227,11 +227,11 @@   forM_ serverMsg $ \case     MsgResponse resp -> sendMsg $ TickerResponse resp     MsgError err -> sendMsg $ TickerError err-    MsgTicker ticker -> liftIO . atomically $ writeTChan groupChannel ticker+    MsgTicker ticker -> atomically $ writeTChan groupChannel ticker  sendWs :: (Show a, ToJSON a) => TChan a -> WS.Connection -> IO () sendWs channel connection = forever $ do-  msg <- liftIO . atomically $ readTChan channel+  msg <- atomically $ readTChan channel   WS.sendTextData connection (encode msg)  main :: IO ()@@ -243,6 +243,7 @@   where     config = [       appWindowTitle "Ticker",+      appWindowIcon "./assets/images/icon.bmp",       appTheme customDarkTheme,       appFontDef "Regular" "./assets/fonts/Roboto-Regular.ttf",       appFontDef "Remix" "./assets/fonts/remixicon.ttf",@@ -271,11 +272,11 @@       xs <- collectJustM action       return (x : xs) -formatTickerValue :: Scientific -> Text-formatTickerValue = T.pack . formatScientific Fixed (Just 8)+formatTickerValue :: FixedFloat -> Text+formatTickerValue = F.sformat (F.fixed 8) -formatTickerPct :: Scientific -> Text-formatTickerPct = T.pack . formatScientific Fixed (Just 2)+formatTickerPct :: FixedFloat -> Text+formatTickerPct = F.sformat (F.fixed 2)  initialList :: [Text] initialList = ["BTCUSDT", "ETHBTC", "BNBBTC", "ADABTC", "DOTBTC", "XRPBTC",
examples/todo/Main.hs view
@@ -221,7 +221,7 @@ addNewTodo :: WidgetEnv s e -> TodoModel -> TodoModel addNewTodo wenv model = newModel where   newTodo = model ^. activeTodo-    & todoId .~ wenv ^. L.timestamp+    & todoId .~ currentTimeMs wenv   newModel = model     & todos .~ (newTodo : model ^. todos) @@ -245,6 +245,7 @@   where     config = [       appWindowTitle "Todo list",+      appWindowIcon "./assets/images/icon.bmp",       appTheme customDarkTheme,       appFontDef "Regular" "./assets/fonts/Roboto-Regular.ttf",       appFontDef "Medium" "./assets/fonts/Roboto-Medium.ttf",
examples/todo/TodoTypes.hs view
@@ -31,7 +31,7 @@   deriving (Eq, Show, Enum)  data Todo = Todo {-  _todoId :: Int,+  _todoId :: Millisecond,   _todoType :: TodoType,   _status :: TodoStatus,   _description :: Text
examples/tutorial/Tutorial01_Basics.hs view
@@ -62,6 +62,7 @@   where     config = [       appWindowTitle "Tutorial 01 - Basics",+      appWindowIcon "./assets/images/icon.bmp",       appTheme darkTheme,       appFontDef "Regular" "./assets/fonts/Roboto-Regular.ttf",       appInitEvent AppInit
examples/tutorial/Tutorial02_Styling.hs view
@@ -106,6 +106,7 @@   where     config = [       appWindowTitle "Tutorial 02 - Styling",+      appWindowIcon "./assets/images/icon.bmp",       appTheme darkTheme,       appFontDef "Regular" "./assets/fonts/Roboto-Regular.ttf",       appFontDef "Medium" "./assets/fonts/Roboto-Medium.ttf",
examples/tutorial/Tutorial03_LifeCycle.hs view
@@ -22,7 +22,7 @@ import qualified Monomer.Lens as L  data ListItem = ListItem {-  _ts :: Int,+  _ts :: Millisecond,   _text :: Text } deriving (Eq, Show) @@ -59,7 +59,8 @@       keystroke [("Enter", AddItem)] $ hstack [         label "Description:",         spacer,-        textField_ newItemText [placeholder "Write here!"], -- `nodeKey` "description",+        textField_ newItemText [placeholder "Write here!"]+          `nodeKey` "description",         spacer,         button "Add" AddItem           `styleBasic` [paddingH 5]@@ -89,7 +90,7 @@     & items .~ removeIdx idx (model ^. items)]   _ -> []   where-    newItem = ListItem (wenv ^. L.timestamp) (model ^. newItemText)+    newItem = ListItem (currentTimeMs wenv) (model ^. newItemText)  removeIdx :: Int -> [a] -> [a] removeIdx idx lst = part1 ++ drop 1 part2 where@@ -101,6 +102,7 @@   where     config = [       appWindowTitle "Tutorial 03 - Merging",+      appWindowIcon "./assets/images/icon.bmp",       appTheme darkTheme,       appFontDef "Regular" "./assets/fonts/Roboto-Regular.ttf",       appInitEvent AppInit
examples/tutorial/Tutorial04_Tasks.hs view
@@ -52,7 +52,7 @@     `nodeVisible` (model ^. selected == idx)   imageSet = hstack [       numberedImage "https://picsum.photos/id/1020/800/600" 1,-      numberedImage "https://picsum.photos/id/1047/800/600" 2,+      numberedImage "https://picsum.photos/id/1043/800/600" 2,       numberedImage "https://picsum.photos/id/1047/800/600" 3,       numberedImage "https://picsum.photos/id/1025/800/600" 4,       numberedImage "https://picsum.photos/id/1080/800/600" 5,@@ -87,6 +87,7 @@   where     config = [       appWindowTitle "Tutorial 04 - Tasks",+      appWindowIcon "./assets/images/icon.bmp",       appTheme darkTheme,       appFontDef "Regular" "./assets/fonts/Roboto-Regular.ttf",       appFontDef "Bold" "./assets/fonts/Roboto-Bold.ttf"
examples/tutorial/Tutorial05_Producers.hs view
@@ -80,6 +80,7 @@   where     config = [       appWindowTitle "Tutorial 05 - Producers",+      appWindowIcon "./assets/images/icon.bmp",       appTheme darkTheme,       appFontDef "Regular" "./assets/fonts/Roboto-Regular.ttf",       appFontDef "Bold" "./assets/fonts/Roboto-Bold.ttf",
examples/tutorial/Tutorial06_Composite.hs view
@@ -150,6 +150,7 @@   where     config = [       appWindowTitle "Tutorial 06 - Composite",+      appWindowIcon "./assets/images/icon.bmp",       appTheme darkTheme,       appFontDef "Regular" "./assets/fonts/Roboto-Regular.ttf",       appInitEvent AppInit
examples/tutorial/Tutorial07_CustomWidget.hs view
@@ -168,6 +168,7 @@   where     config = [       appWindowTitle "Tutorial 07 - Custom Widget",+      appWindowIcon "./assets/images/icon.bmp",       appTheme darkTheme,       appFontDef "Regular" "./assets/fonts/Roboto-Regular.ttf"       ]
examples/tutorial/Tutorial08_Themes.hs view
@@ -115,6 +115,7 @@   where     config = [       appWindowTitle "Tutorial 08 - Themes",+      appWindowIcon "./assets/images/icon.bmp",       appTheme darkTheme,       appFontDef "Regular" "./assets/fonts/Roboto-Regular.ttf",       appInitEvent AppInit
monomer.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack  name:           monomer-version:        1.3.0.0+version:        1.4.0.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.@@ -156,7 +156,7 @@       c2hs   build-depends:       JuicyPixels >=3.2.9 && <3.5-    , OpenGL ==3.0.*+    , OpenGLRaw ==3.3.*     , async >=2.1 && <2.3     , attoparsec >=0.12 && <0.15     , base >=4.11 && <5@@ -172,14 +172,12 @@     , mtl >=2.1 && <2.3     , nanovg >=0.8 && <1.0     , process ==1.6.*-    , safe ==0.3.*     , sdl2 >=2.5.0 && <2.6     , stm ==2.5.*     , text >=1.2 && <2.1     , text-show >=3.7 && <3.10     , time >=1.8 && <1.16     , transformers >=0.5 && <0.7-    , unordered-containers >=0.2.8 && <0.3     , vector >=0.12 && <0.14     , wreq >=0.5.2 && <0.6   if os(windows)@@ -202,7 +200,7 @@   ghc-options: -threaded   build-depends:       JuicyPixels >=3.2.9 && <3.5-    , OpenGL ==3.0.*+    , OpenGLRaw ==3.3.*     , aeson >=1.4 && <2.3     , async >=2.1 && <2.3     , attoparsec >=0.12 && <0.15@@ -220,14 +218,12 @@     , mtl >=2.1 && <2.3     , nanovg >=0.8 && <1.0     , process ==1.6.*-    , safe ==0.3.*     , sdl2 >=2.5.0 && <2.6     , stm ==2.5.*     , text >=1.2 && <2.1     , text-show >=3.7 && <3.10     , time >=1.8 && <1.16     , transformers >=0.5 && <0.7-    , unordered-containers >=0.2.8 && <0.3     , vector >=0.12 && <0.14     , wreq >=0.5.2 && <0.6   default-language: Haskell2010@@ -246,7 +242,7 @@   ghc-options: -threaded   build-depends:       JuicyPixels >=3.2.9 && <3.5-    , OpenGL ==3.0.*+    , OpenGLRaw ==3.3.*     , async >=2.1 && <2.3     , attoparsec >=0.12 && <0.15     , base >=4.11 && <5@@ -264,14 +260,12 @@     , nanovg >=0.8 && <1.0     , process ==1.6.*     , random >=1.1 && <1.3-    , safe ==0.3.*     , sdl2 >=2.5.0 && <2.6     , stm ==2.5.*     , text >=1.2 && <2.1     , text-show >=3.7 && <3.10     , time >=1.8 && <1.16     , transformers >=0.5 && <0.7-    , unordered-containers >=0.2.8 && <0.3     , vector >=0.12 && <0.14     , wreq >=0.5.2 && <0.6   default-language: Haskell2010@@ -288,7 +282,6 @@   ghc-options: -threaded   build-depends:       JuicyPixels >=3.2.9 && <3.5-    , OpenGL ==3.0.*     , OpenGLRaw ==3.3.*     , async >=2.1 && <2.3     , attoparsec >=0.12 && <0.15@@ -307,14 +300,12 @@     , nanovg >=0.8 && <1.0     , process ==1.6.*     , random >=1.1 && <1.3-    , safe ==0.3.*     , sdl2 >=2.5.0 && <2.6     , stm ==2.5.*     , text >=1.2 && <2.1     , text-show >=3.7 && <3.10     , time >=1.8 && <1.16     , transformers >=0.5 && <0.7-    , unordered-containers >=0.2.8 && <0.3     , vector >=0.12 && <0.14     , wreq >=0.5.2 && <0.6   default-language: Haskell2010@@ -332,7 +323,7 @@   ghc-options: -threaded   build-depends:       JuicyPixels >=3.2.9 && <3.5-    , OpenGL ==3.0.*+    , OpenGLRaw ==3.3.*     , aeson >=1.4 && <2.3     , async >=2.1 && <2.3     , attoparsec >=0.12 && <0.15@@ -350,15 +341,12 @@     , mtl >=2.1 && <2.3     , nanovg >=0.8 && <1.0     , process ==1.6.*-    , safe ==0.3.*-    , scientific ==0.3.*     , sdl2 >=2.5.0 && <2.6     , stm ==2.5.*     , text >=1.2 && <2.1     , text-show >=3.7 && <3.10     , time >=1.8 && <1.16     , transformers >=0.5 && <0.7-    , unordered-containers >=0.2.8 && <0.3     , vector >=0.12 && <0.14     , websockets ==0.12.*     , wreq >=0.5.2 && <0.6@@ -377,7 +365,7 @@   ghc-options: -threaded   build-depends:       JuicyPixels >=3.2.9 && <3.5-    , OpenGL ==3.0.*+    , OpenGLRaw ==3.3.*     , async >=2.1 && <2.3     , attoparsec >=0.12 && <0.15     , base >=4.11 && <5@@ -394,14 +382,12 @@     , mtl >=2.1 && <2.3     , nanovg >=0.8 && <1.0     , process ==1.6.*-    , safe ==0.3.*     , sdl2 >=2.5.0 && <2.6     , stm ==2.5.*     , text >=1.2 && <2.1     , text-show >=3.7 && <3.10     , time >=1.8 && <1.16     , transformers >=0.5 && <0.7-    , unordered-containers >=0.2.8 && <0.3     , vector >=0.12 && <0.14     , wreq >=0.5.2 && <0.6   default-language: Haskell2010@@ -425,7 +411,7 @@   ghc-options: -threaded   build-depends:       JuicyPixels >=3.2.9 && <3.5-    , OpenGL ==3.0.*+    , OpenGLRaw ==3.3.*     , async >=2.1 && <2.3     , attoparsec >=0.12 && <0.15     , base >=4.11 && <5@@ -443,14 +429,12 @@     , nanovg >=0.8 && <1.0     , process ==1.6.*     , random >=1.1 && <1.3-    , safe ==0.3.*     , sdl2 >=2.5.0 && <2.6     , stm ==2.5.*     , text >=1.2 && <2.1     , text-show >=3.7 && <3.10-    , time >=1.8 && <1.13+    , time >=1.8 && <1.16     , transformers >=0.5 && <0.7-    , unordered-containers >=0.2.8 && <0.3     , vector >=0.12 && <0.14     , wreq >=0.5.2 && <0.6   default-language: Haskell2010@@ -461,6 +445,7 @@   other-modules:       Monomer.Common.CursorIconSpec       Monomer.Core.SizeReqSpec+      Monomer.Core.StyleUtilSpec       Monomer.Graphics.UtilSpec       Monomer.TestEventUtil       Monomer.TestUtil@@ -512,9 +497,8 @@       OverloadedStrings   ghc-options: -fwarn-incomplete-patterns   build-depends:-      HUnit ==1.6.*-    , JuicyPixels >=3.2.9 && <3.5-    , OpenGL ==3.0.*+      JuicyPixels >=3.2.9 && <3.5+    , OpenGLRaw ==3.3.*     , async >=2.1 && <2.3     , attoparsec >=0.12 && <0.15     , base >=4.11 && <5@@ -522,7 +506,6 @@     , bytestring-to-vector ==0.3.*     , containers >=0.5.11 && <0.7     , data-default >=0.5 && <0.8-    , directory ==1.3.*     , exceptions ==0.10.*     , extra >=1.6 && <1.9     , formatting >=6.0 && <8.0@@ -533,15 +516,12 @@     , mtl >=2.1 && <2.3     , nanovg >=0.8 && <1.0     , process ==1.6.*-    , safe ==0.3.*     , sdl2 >=2.5.0 && <2.6-    , silently ==1.2.*     , stm ==2.5.*     , text >=1.2 && <2.1     , text-show >=3.7 && <3.10     , time >=1.8 && <1.16     , transformers >=0.5 && <0.7-    , unordered-containers >=0.2.8 && <0.3     , vector >=0.12 && <0.14     , wreq >=0.5.2 && <0.6   default-language: Haskell2010
src/Monomer.hs view
@@ -10,7 +10,7 @@ that should be imported by applications.  To start using the library, it is recommended to check the-<https://github.com/fjvallarino/monomer#documentation tutorials>:+<https://github.com/fjvallarino/monomer#documentation tutorials>.  If you don't want to use all the helper modules, or you don't want to import them unqualified, the modules you will need to import to create an application
src/Monomer/Common/BasicTypes.hs view
@@ -165,6 +165,14 @@   px2 = max rx . min (rx + rw) $ px   py2 = max ry . min (ry + rh) $ py +-- | Returns a rect using the provided points as boundaries+rectFromPoints :: Point -> Point -> Rect+rectFromPoints (Point x1 y1) (Point x2 y2) = Rect x y w h where+  x = min x1 x2+  y = min y1 y2+  w = abs (x2 - x1)+  h = abs (y2 - y1)+ -- | Adds individual x, y, w and h coordinates to a rect. addToRect :: Rect -> Double -> Double -> Double -> Double -> Maybe Rect addToRect (Rect x y w h) l r t b = newRect where
src/Monomer/Core/Combinators.hs view
@@ -30,16 +30,40 @@  {-| Given two values, usually model, checks if merge is required for a given widget.-The first parameter corresponds to the old value, and the second to the new.++The first parameter usually corresponds to the current 'WidgetEnv', the second+to the old value/model, and the third to the new/model.++This is used, for example, by _composite_ and _box_. -}-class CmbMergeRequired t s | t -> s where-  mergeRequired :: (s -> s -> Bool) -> t+class CmbMergeRequired t w s | t -> w s where+  mergeRequired :: (w -> s -> s -> Bool) -> t --- | Listener for the validation status of a field using a lens.+{-|+Listener for the validation status of a user input field using a lens.++Allows associating a flag to know if the input of a field with validation+settings is valid. This can be used with _textField_, _numericField_,+_dateField_ and _timeField_.++The flag can be used for styling the component according to the current status.+Beyond styling, its usage is needed for validation purposes. Taking+_numericField_ as an example, one can bind a 'Double' record field to it and set+a valid range from 0 to 100. When the user inputs 100, the record field will+reflect the correct value. If the user adds a 0 (the numericField showing 1000),+the record field will still have 100 because it's the last valid value. Since+there is not a way of indicating errors when using primitive types (a 'Double'+is just a number), we can rely on the flag to check its validity.+-} class CmbValidInput t s | t -> s where   validInput :: ALens' s Bool -> t --- | Listener for the validation status of a field using an event handler.+{-|+Listener for the validation status of a user input field using an event handler,+avoiding the need of a lens.++Check 'CmbValidInput' for details.+-} class CmbValidInputV t e | t -> e where   validInputV :: (Bool -> e) -> t @@ -49,6 +73,16 @@   selectOnFocus = selectOnFocus_ True   selectOnFocus_ :: Bool -> t +{-|+Defines whether a widget prevents the user changing the value. Note that, in+contrast to a disabled widget, a read-only widget can still be focused and+still allows selecting and copying the value.+-}+class CmbReadOnly t where+  readOnly :: t+  readOnly = readOnly_ True+  readOnly_ :: Bool -> t+ -- | Defines whether a widget changes its size when the model changes. class CmbResizeOnChange t where   resizeOnChange :: t@@ -249,6 +283,10 @@   textThroughline = textThroughline_ True   textThroughline_ :: Bool -> t +-- | How to break texts into lines.+class CmbTextLineBreak t where+  textLineBreak :: LineBreak -> t+ -- | Does not apply any kind of resizing to fit to container. class CmbFitNone t where   fitNone :: t@@ -302,8 +340,8 @@   thumbHoverColor :: Color -> t  {-|-The thumb factor. For example, in slider this makes the thumb proportional-to the width of the slider.+The thumb factor. For example, in slider this makes the thumb proportional to+the width of the slider. -} class CmbThumbFactor t where   thumbFactor :: Double -> t@@ -318,30 +356,71 @@   thumbVisible = thumbVisible_ True   thumbVisible_ :: Bool -> t --- | The width color of a thumb, for example in a scroll.+-- | The width of a thumb, for example in a scroll. class CmbThumbWidth t where   thumbWidth :: Double -> t +-- | The minimum size of a thumb, for example in a scroll.+class CmbThumbMinSize t where+  thumbMinSize :: Double -> t+ -- | Whether to show an alpha channel, for instance in color selector. class CmbShowAlpha t where   showAlpha :: t   showAlpha = showAlpha_ True   showAlpha_ :: Bool -> t --- | Whether to ignore children events.+{-|+Whether to ignore children events.++By default low-level events (keyboard, mouse, clipboard, etc) traverse the whole+branch where the target widget is located in the widget tree, giving the chance+to each widget along the line to respond to the event.++In some cases it is desirable to restrict which widgets can handle an event. Two+different 'WidgetRequest's, which can be returned during event handling, exist+for this:++- 'IgnoreChildrenEvents': parent widgets always have the priority. If a widget+  returns this 'WidgetRequest' during event handling, its children widgets+  response will be ignored. For example, the _keystroke_ widget can be+  configured to return this when a keystroke combination matches.+- 'IgnoreParentEvents': if no parent widget requested 'IgnoreChildrenEvents', a+  widget can respond with 'IgnoreParentEvents' to have its response being the+  only one taking place. This is used, for example, by the _textArea_ widget to+  handle the tab key; without this, the default handler would pass focus to the+  next widget down the line.++Some of the stock widgets allow configuring this behavior (e.g, keystroke and+button).+-} class CmbIgnoreChildrenEvts t where   ignoreChildrenEvts :: t   ignoreChildrenEvts = ignoreChildrenEvts_ True   ignoreChildrenEvts_ :: Bool -> t +-- | Whether to ignore parent events. Check 'CmbIgnoreChildrenEvts'.+class CmbIgnoreParentEvts t where+  ignoreParentEvts :: t+  ignoreParentEvts = ignoreParentEvts_ True+  ignoreParentEvts_ :: Bool -> t+ -- | On init event. class CmbOnInit t e | t -> e where   onInit :: e -> t +-- | On init WidgetRequest.+class CmbOnInitReq t s e | t -> s e where+  onInitReq :: WidgetRequest s e -> t+ -- | On dispose event. class CmbOnDispose t e | t -> e where   onDispose :: e -> t +-- | On dispose WidgetRequest.+class CmbOnDisposeReq t s e | t -> s e where+  onDisposeReq :: WidgetRequest s e -> t+ -- | On resize event. class CmbOnResize t e a | t -> e a where   onResize :: (a -> e) -> t@@ -515,35 +594,93 @@  -- Style infixl 5 `styleBasic`+infixl 5 `styleBasicSet`+ infixl 5 `styleHover`+infixl 5 `styleHoverSet`+ infixl 5 `styleFocus`+infixl 5 `styleFocusSet`+ infixl 5 `styleFocusHover`+infixl 5 `styleFocusHoverSet`+ infixl 5 `styleActive`+infixl 5 `styleActiveSet`+ infixl 5 `styleDisabled`+infixl 5 `styleDisabledSet` --- | Basic style combinator, used mainly infix for widgets as a list.+{-|+Basic style combinator, mainly used infix with widgets.++Represents the default style of a widget. It serves as the base for all the+other style states when an attribute is not overriden.+-} class CmbStyleBasic t where+  -- | Merges the new basic style states with the existing ones.   styleBasic :: t -> [StyleState] -> t+  -- | Sets the new basic style states overriding the existing ones.+  styleBasicSet :: t -> [StyleState] -> t --- | Hover style combinator, used mainly infix for widgets as a list.+{-|+Hover style combinator, mainly used infix with widgets.++Used when the widget is hovered with a pointing device.+-} class CmbStyleHover t where+  -- | Merges the new hover style states with the existing ones.   styleHover :: t -> [StyleState] -> t+  -- | Sets the new hover style states overriding the existing ones.+  styleHoverSet :: t -> [StyleState] -> t --- | Focus style combinator, used mainly infix for widgets as a list.+{-|+Focus style combinator, mainly used infix with widgets.++Used when the widget has keyboard focus.+-} class CmbStyleFocus t where+  -- | Merges the new focus style states with the existing ones.   styleFocus :: t -> [StyleState] -> t+  -- | Sets the new focus style states overriding the existing ones.+  styleFocusSet :: t -> [StyleState] -> t --- | Focus Hover style combinator, used mainly infix for widgets as a list.+{-|+Focus Hover style combinator, mainly used infix with widgets.++Used when the widget is both focused and hovered. In this situation the+attributes defined in focus and hover will be combined, with focus attributes+taking precedence. This style state allows for better control in cases when the+combination of focus and hover styles do not match expectations.+-} class CmbStyleFocusHover t where+  -- | Merges the new focus hover style states with the existing ones.   styleFocusHover :: t -> [StyleState] -> t+  -- | Sets the new focus hover style states overriding the existing ones.+  styleFocusHoverSet :: t -> [StyleState] -> t --- | Active style combinator, used mainly infix for widgets as a list.+{-|+Active style combinator, mainly used infix with widgets.++Used when a mouse press was started in the widget and the pointer is inside its+boundaries.+-} class CmbStyleActive t where+  -- | Merges the new active style states with the existing ones.   styleActive :: t -> [StyleState] -> t+  -- | Sets the new active style states overriding the existing ones.+  styleActiveSet :: t -> [StyleState] -> t --- | Disabled style combinator, used mainly infix for widgets as a list.+{-|+Disabled style combinator, mainly used infix with widgets.++Used when the _nodeEnabled_ attribute has been set to False.+-} class CmbStyleDisabled t where+  -- | Merges the new disabled style states with the existing ones.   styleDisabled :: t -> [StyleState] -> t+  -- | Sets the new disabled style states overriding the existing ones.+  styleDisabledSet :: t -> [StyleState] -> t  -- | Ignore theme settings and start with blank style. class CmbIgnoreTheme t where
src/Monomer/Core/SizeReq.hs view
@@ -13,6 +13,8 @@ module Monomer.Core.SizeReq (   SizeReqUpdater(..),   clearExtra,+  clearExtraW,+  clearExtraH,   fixedToMinW,   fixedToMinH,   fixedToMaxW,@@ -53,12 +55,29 @@ clearExtra :: SizeReqUpdater clearExtra (reqW, reqH) = (reqW & L.extra .~ 0, reqH & L.extra .~ 0) +-- | Clears the horizontal extra field of a pair of SizeReqs.+clearExtraW :: SizeReqUpdater+clearExtraW (reqW, reqH) = (reqW & L.extra .~ 0, reqH)++-- | Clears the vertical extra field of a pair of SizeReqs.+clearExtraH :: SizeReqUpdater+clearExtraH (reqW, reqH) = (reqW, reqH & L.extra .~ 0)++-- | Switches a SizeReq pair from fixed size to minimum size.+fixedToMin+  :: Double          -- ^ The resize factor.+  -> SizeReqUpdater  -- ^ The updated SizeReq.+fixedToMin fs (reqW, reqH) = (newReqW, newReqH) where+  (fixedW, fixedH) = (reqW ^. L.fixed, reqH ^. L.fixed)+  newReqW = SizeReq fixedW 0 fixedW fs+  newReqH = SizeReq fixedH 0 fixedH fs+ -- | Switches a SizeReq pair from fixed width to minimum width. fixedToMinW   :: Double          -- ^ The resize factor.   -> SizeReqUpdater  -- ^ The updated SizeReq.-fixedToMinW fw (SizeReq fixed _ _ _, reqH) = (newReqH, reqH) where-  newReqH = SizeReq fixed 0 fixed fw+fixedToMinW fw (SizeReq fixed _ _ _, reqH) = (newReqW, reqH) where+  newReqW = SizeReq fixed 0 fixed fw  -- | Switches a SizeReq pair from fixed height to minimum height. fixedToMinH@@ -67,12 +86,21 @@ fixedToMinH fh (reqW, SizeReq fixed _ _ _) = (reqW, newReqH) where   newReqH = SizeReq fixed 0 fixed fh +-- | Switches a SizeReq pair from fixed size to maximum size.+fixedToMax+  :: Double          -- ^ The resize factor.+  -> SizeReqUpdater  -- ^ The updated SizeReq.+fixedToMax fs (reqW, reqH) = (newReqW, newReqH) where+  (fixedW, fixedH) = (reqW ^. L.fixed, reqH ^. L.fixed)+  newReqW = SizeReq 0 fixedW 0 fs+  newReqH = SizeReq 0 fixedH 0 fs+ -- | Switches a SizeReq pair from fixed width to maximum width. fixedToMaxW   :: Double          -- ^ The resize factor.   -> SizeReqUpdater  -- ^ The updated SizeReq.-fixedToMaxW fw (SizeReq fixed _ _ _, reqH) = (newReqH, reqH) where-  newReqH = SizeReq 0 fixed 0 fw+fixedToMaxW fw (SizeReq fixed _ _ _, reqH) = (newReqW, reqH) where+  newReqW = SizeReq 0 fixed 0 fw  -- | Switches a SizeReq pair from fixed height to maximum height. fixedToMaxH@@ -81,12 +109,21 @@ fixedToMaxH fh (reqW, SizeReq fixed _ _ _) = (reqW, newReqH) where   newReqH = SizeReq 0 fixed 0 fh +-- | Switches a SizeReq pair from fixed size to expand size.+fixedToExpand+  :: Double          -- ^ The resize factor.+  -> SizeReqUpdater  -- ^ The updated SizeReq.+fixedToExpand fs (reqW, reqH) = (newReqW, newReqH) where+  (fixedW, fixedH) = (reqW ^. L.fixed, reqH ^. L.fixed)+  newReqW = SizeReq 0 fixedW fixedW fs+  newReqH = SizeReq 0 fixedH fixedH fs+ -- | Switches a SizeReq pair from fixed width to expand width. fixedToExpandW   :: Double          -- ^ The resize factor.   -> SizeReqUpdater  -- ^ The updated SizeReq.-fixedToExpandW fw (SizeReq fixed _ _ _, reqH) = (newReqH, reqH) where-  newReqH = SizeReq 0 fixed fixed fw+fixedToExpandW fw (SizeReq fixed _ _ _, reqH) = (newReqW, reqH) where+  newReqW = SizeReq 0 fixed fixed fw  -- | Switches a SizeReq pair from fixed height to expand height. fixedToExpandH
src/Monomer/Core/Style.hs view
@@ -177,6 +177,9 @@ instance CmbTextThroughline TextStyle where   textThroughline_ through = def & L.throughline ?~ through +instance CmbTextLineBreak TextStyle where+  textLineBreak break = def & L.lineBreak ?~ break+ -- Padding  instance CmbPadding Padding where@@ -360,6 +363,9 @@ instance CmbTextThroughline StyleState where   textThroughline_ False = def   textThroughline_ True = def & L.text ?~ textThroughline++instance CmbTextLineBreak StyleState where+  textLineBreak break = def & L.text ?~ textLineBreak break  -- Padding instance CmbPadding StyleState where
src/Monomer/Core/StyleTypes.hs view
@@ -306,6 +306,15 @@ instance Monoid Radius where   mempty = def +-- | Defines how to break texts into lines.+data LineBreak+  = OnSpaces+  | OnCharacters+  deriving (Eq, Ord, Enum, Show, Generic)++instance Default LineBreak where+  def = OnSpaces+ -- | Text related definitions. data TextStyle = TextStyle {   _txsFont :: Maybe Font,          -- ^ The font type.@@ -317,7 +326,8 @@   _txsOverline :: Maybe Bool,      -- ^ True if overline should be displayed.   _txsThroughline :: Maybe Bool,   -- ^ True if throughline should be displayed.   _txsAlignH :: Maybe AlignTH,     -- ^ Horizontal alignment.-  _txsAlignV :: Maybe AlignTV      -- ^ Vertical alignment.+  _txsAlignV :: Maybe AlignTV,     -- ^ Vertical alignment.+  _txsLineBreak :: Maybe LineBreak -- ^ Line break option. } deriving (Eq, Show, Generic)  instance Default TextStyle where@@ -331,7 +341,8 @@     _txsOverline = Nothing,     _txsThroughline = Nothing,     _txsAlignH = Nothing,-    _txsAlignV = Nothing+    _txsAlignV = Nothing,+    _txsLineBreak = Nothing   }  instance Semigroup TextStyle where@@ -345,7 +356,8 @@     _txsOverline = _txsOverline ts2 <|> _txsOverline ts1,     _txsThroughline = _txsThroughline ts2 <|> _txsThroughline ts1,     _txsAlignH = _txsAlignH ts2 <|> _txsAlignH ts1,-    _txsAlignV = _txsAlignV ts2 <|> _txsAlignV ts1+    _txsAlignV = _txsAlignV ts2 <|> _txsAlignV ts1,+    _txsLineBreak = _txsLineBreak ts2 <|> _txsLineBreak ts1   }  instance Monoid TextStyle where
src/Monomer/Core/StyleUtil.hs view
@@ -8,8 +8,10 @@  Helper functions for style types. -}+{-# LANGUAGE BangPatterns #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE RankNTypes #-} {-# LANGUAGE Strict #-}  module Monomer.Core.StyleUtil (@@ -25,6 +27,7 @@   styleFontColor,   styleTextAlignH,   styleTextAlignV,+  styleTextLineBreak,   styleBgColor,   styleFgColor,   styleSndColor,@@ -42,7 +45,7 @@   mapStyleStates ) where -import Control.Lens ((&), (^.), (^?), (.~), (+~), (%~), (?~), _Just, non)+import Control.Lens import Data.Default import Data.Maybe import Data.Text (Text)@@ -58,64 +61,88 @@  instance CmbStyleBasic Style where   styleBasic oldStyle states = newStyle where+    newStyle = oldStyle & L.basic <>~ maybeConcat states++  styleBasicSet oldStyle states = newStyle where     newStyle = oldStyle & L.basic .~ maybeConcat states  instance CmbStyleHover Style where   styleHover oldStyle states = newStyle where+    newStyle = oldStyle & L.hover <>~ maybeConcat states++  styleHoverSet oldStyle states = newStyle where     newStyle = oldStyle & L.hover .~ maybeConcat states  instance CmbStyleFocus Style where   styleFocus oldStyle states = newStyle where+    newStyle = oldStyle & L.focus <>~ maybeConcat states++  styleFocusSet oldStyle states = newStyle where     newStyle = oldStyle & L.focus .~ maybeConcat states  instance CmbStyleFocusHover Style where   styleFocusHover oldStyle states = newStyle where+    newStyle = oldStyle & L.focusHover <>~ maybeConcat states++  styleFocusHoverSet oldStyle states = newStyle where     newStyle = oldStyle & L.focusHover .~ maybeConcat states  instance CmbStyleActive Style where   styleActive oldStyle states = newStyle where+    newStyle = oldStyle & L.active <>~ maybeConcat states++  styleActiveSet oldStyle states = newStyle where     newStyle = oldStyle & L.active .~ maybeConcat states  instance CmbStyleDisabled Style where   styleDisabled oldStyle states = newStyle where+    newStyle = oldStyle & L.disabled <>~ maybeConcat states++  styleDisabledSet oldStyle states = newStyle where     newStyle = oldStyle & L.disabled .~ maybeConcat states  instance CmbStyleBasic (WidgetNode s e) where-  styleBasic node states = node & L.info . L.style .~ newStyle where-    state = mconcat states-    oldStyle = node ^. L.info . L.style-    newStyle = oldStyle & L.basic ?~ state+  styleBasic node states = newNode where+    newNode = mergeNodeStyleState L.basic node states +  styleBasicSet node states = newNode where+    newNode = setNodeStyleState L.basic node states+ instance CmbStyleHover (WidgetNode s e) where-  styleHover node states = node & L.info . L.style .~ newStyle where-    state = mconcat states-    oldStyle = node ^. L.info . L.style-    newStyle = oldStyle & L.hover ?~ state+  styleHover node states = newNode where+    newNode = mergeNodeStyleState L.hover node states +  styleHoverSet node states = newNode where+    newNode = setNodeStyleState L.hover node states+ instance CmbStyleFocus (WidgetNode s e) where-  styleFocus node states = node & L.info . L.style .~ newStyle where-    state = mconcat states-    oldStyle = node ^. L.info . L.style-    newStyle = oldStyle & L.focus ?~ state+  styleFocus node states = newNode where+    newNode = mergeNodeStyleState L.focus node states +  styleFocusSet node states = newNode where+    newNode = setNodeStyleState L.focus node states+ instance CmbStyleFocusHover (WidgetNode s e) where-  styleFocusHover node states = node & L.info . L.style .~ newStyle where-    state = mconcat states-    oldStyle = node ^. L.info . L.style-    newStyle = oldStyle & L.focusHover ?~ state+  styleFocusHover node states = newNode where+    newNode = mergeNodeStyleState L.focusHover node states +  styleFocusHoverSet node states = newNode where+    newNode = setNodeStyleState L.focusHover node states+ instance CmbStyleActive (WidgetNode s e) where-  styleActive node states = node & L.info . L.style .~ newStyle where-    state = mconcat states-    oldStyle = node ^. L.info . L.style-    newStyle = oldStyle & L.active ?~ state+  styleActive node states = newNode where+    newNode = mergeNodeStyleState L.active node states +  styleActiveSet node states = newNode where+    newNode = setNodeStyleState L.active node states+ instance CmbStyleDisabled (WidgetNode s e) where-  styleDisabled node states = node & L.info . L.style .~ newStyle where-    state = mconcat states-    oldStyle = node ^. L.info . L.style-    newStyle = oldStyle & L.disabled ?~ state+  styleDisabled node states = newNode where+    newNode = mergeNodeStyleState L.disabled node states +  styleDisabledSet node states = newNode where+    newNode = setNodeStyleState L.disabled node states+ infixl 5 `nodeKey` infixl 5 `nodeEnabled` infixl 5 `nodeVisible`@@ -177,6 +204,11 @@ styleTextAlignV style = fromMaybe def alignV where   alignV = style ^? L.text . _Just . L.alignV . _Just +-- | Returns the line break option of the given style state, or the+styleTextLineBreak :: StyleState -> LineBreak+styleTextLineBreak style = fromMaybe def lineBreak where+  lineBreak = style ^? L.text . _Just . L.lineBreak . _Just+ -- | Returns the background color of the given style state, or the default. styleBgColor :: StyleState -> Color styleBgColor style = fromMaybe def bgColor where@@ -306,6 +338,34 @@   br = maybe 0 _bsWidth (_brdRight border)   bt = maybe 0 _bsWidth (_brdTop border)   bb = maybe 0 _bsWidth (_brdBottom border)++mergeNodeStyleState+  :: Lens' Style (Maybe StyleState)+  -> WidgetNode s e+  -> [StyleState]+  -> WidgetNode s e+mergeNodeStyleState field node states = newNode where+  oldStyle = node ^. L.info . L.style+  oldState = oldStyle ^. field . non def+  !mcatStates = mconcat states+  !newStates = oldState <> mcatStates+  !newStyle = oldStyle+    & field ?~ newStates+  !newNode = node+    & L.info . L.style .~ newStyle++setNodeStyleState+  :: Lens' Style (Maybe StyleState)+  -> WidgetNode s e+  -> [StyleState]+  -> WidgetNode s e+setNodeStyleState field node states = newNode where+  oldStyle = node ^. L.info . L.style+  !newStates = mconcat states+  !newStyle = oldStyle+    & field ?~ newStates+  !newNode = node+    & L.info . L.style .~ newStyle  justDef :: (Default a) => Maybe a -> a justDef val = fromMaybe def val
src/Monomer/Core/ThemeTypes.hs view
@@ -84,6 +84,7 @@   _thsScrollThumbColor :: Color,   _thsScrollBarWidth :: Double,   _thsScrollThumbWidth :: Double,+  _thsScrollThumbMinSize :: Double,   _thsScrollThumbRadius :: Double,   _thsScrollWheelRate :: Rational,   _thsSeparatorLineWidth :: Double,@@ -140,6 +141,7 @@     _thsScrollThumbColor = darkGray,     _thsScrollBarWidth = 10,     _thsScrollThumbWidth = 8,+    _thsScrollThumbMinSize = 25,     _thsScrollThumbRadius = 0,     _thsScrollWheelRate = 10,     _thsSeparatorLineWidth = 1,@@ -192,10 +194,11 @@     _thsRadioWidth = _thsRadioWidth t2,     _thsScrollOverlay = _thsScrollOverlay t2,     _thsScrollFollowFocus = _thsScrollFollowFocus t2,-    _thsScrollBarColor =  _thsScrollBarColor t2,-    _thsScrollThumbColor =  _thsScrollThumbColor t2,-    _thsScrollBarWidth =  _thsScrollBarWidth t2,-    _thsScrollThumbWidth =  _thsScrollThumbWidth t2,+    _thsScrollBarColor = _thsScrollBarColor t2,+    _thsScrollThumbColor = _thsScrollThumbColor t2,+    _thsScrollBarWidth = _thsScrollBarWidth t2,+    _thsScrollThumbWidth = _thsScrollThumbWidth t2,+    _thsScrollThumbMinSize = _thsScrollThumbMinSize t2,     _thsScrollThumbRadius = _thsScrollThumbRadius t2,     _thsScrollWheelRate = _thsScrollWheelRate t2,     _thsSeparatorLineWidth = _thsSeparatorLineWidth t2,
src/Monomer/Core/Themes/BaseTheme.hs view
@@ -131,13 +131,29 @@ normalFont = def   & L.font ?~ Font "Regular"   & L.fontSize ?~ FontSize 16-  & L.fontSpaceV ?~ FontSpace 4+  & L.fontSpaceV ?~ FontSpace 2  titleFont :: TextStyle titleFont = def   & L.font ?~ Font "Bold"   & L.fontSize ?~ FontSize 20+  & L.fontSpaceV ?~ FontSpace 2 +dialogMsgBodyFont :: BaseThemeColors -> TextStyle+dialogMsgBodyFont themeMod = fontStyle where+  fontStyle = normalFont+    & L.fontColor ?~ dialogText themeMod++externalLinkFont :: BaseThemeColors -> TextStyle+externalLinkFont themeMod = fontStyle where+  fontStyle = normalFont+    & L.fontColor ?~ externalLinkBasic themeMod++labelFont :: BaseThemeColors -> TextStyle+labelFont themeMod = fontStyle <> textLeft where+  fontStyle = normalFont+    & L.fontColor ?~ labelText themeMod+ btnStyle :: BaseThemeColors -> StyleState btnStyle themeMod = def   & L.text ?~ (normalFont & L.fontColor ?~ btnText themeMod) <> textCenter@@ -153,15 +169,18 @@   & L.border ?~ border 1 (btnMainBgBasic themeMod)  textInputStyle :: BaseThemeColors -> StyleState-textInputStyle themeMod = def-  & L.text ?~ (normalFont & L.fontColor ?~ inputText themeMod)-  & L.bgColor ?~ inputBgBasic themeMod-  & L.fgColor ?~ inputFgBasic themeMod-  & L.sndColor ?~ (inputSndBasic themeMod & L.a .~ 0.6)-  & L.hlColor ?~ inputSelBasic themeMod-  & L.border ?~ border 1 (inputBorder themeMod)-  & L.radius ?~ radius 4-  & L.padding ?~ padding 8+textInputStyle themeMod = style where+  textStyle = normalFont+    & L.fontColor ?~ inputText themeMod+  style = def+    & L.text ?~ textStyle+    & L.bgColor ?~ inputBgBasic themeMod+    & L.fgColor ?~ inputFgBasic themeMod+    & L.sndColor ?~ (inputSndBasic themeMod & L.a .~ 0.6)+    & L.hlColor ?~ inputSelBasic themeMod+    & L.border ?~ border 1 (inputBorder themeMod)+    & L.radius ?~ radius 4+    & L.padding ?~ padding 8  numericInputStyle :: BaseThemeColors -> StyleState numericInputStyle themeMod = textInputStyle themeMod@@ -226,8 +245,7 @@   & L.dialogCloseIconStyle . L.sizeReqH ?~ width 16   & L.dialogButtonsStyle . L.padding ?~ padding 20 <> paddingT 10   & L.dialogMsgBodyStyle . L.padding ?~ padding 20-  & L.dialogMsgBodyStyle . L.text-    ?~ (normalFont & L.fontColor ?~ dialogText themeMod)+  & L.dialogMsgBodyStyle . L.text ?~ dialogMsgBodyFont themeMod   & L.dialogMsgBodyStyle . L.sizeReqW ?~ maxWidth 600   & L.dropdownStyle .~ textInputStyle themeMod   & L.dropdownStyle . L.fgColor ?~ inputIconFg themeMod@@ -236,9 +254,8 @@   & L.dropdownListStyle . L.bgColor ?~ slMainBg themeMod   & L.dropdownItemStyle .~ selectListItemStyle themeMod   & L.dropdownItemSelectedStyle .~ selectListItemSelectedStyle themeMod-  & L.externalLinkStyle . L.text ?~ (normalFont & L.fontColor ?~ externalLinkBasic themeMod)-  & L.labelStyle . L.text-    ?~ (normalFont & L.fontColor ?~ labelText themeMod) <> textLeft+  & L.externalLinkStyle . L.text ?~ externalLinkFont themeMod+  & L.labelStyle . L.text ?~ labelFont themeMod   & L.numericFieldStyle .~ numericInputStyle themeMod   & L.optionBtnOnStyle .~ btnMainStyle themeMod   & L.optionBtnOffStyle .~ btnStyle themeMod@@ -255,6 +272,7 @@   & L.scrollThumbColor .~ scrollThumbBasic themeMod   & L.scrollBarWidth .~ 8   & L.scrollThumbWidth .~ 8+  & L.scrollThumbMinSize .~ 25   & L.scrollThumbRadius .~ 4   & L.scrollWheelRate .~ 10   & L.separatorLineWidth .~ 1
src/Monomer/Core/Util.hs view
@@ -8,11 +8,12 @@  Helper functions for Core types. -}+{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE LambdaCase #-}  module Monomer.Core.Util where -import Control.Lens ((&), (^.), (.~), (?~))+import Control.Lens ((&), (^.), (^?), (.~), (?~), _Just) import Data.Maybe import Data.Text (Text) import Data.Typeable (cast)@@ -29,32 +30,78 @@  import qualified Monomer.Core.Lens as L --- | Returns the path associated to a given key, if any.+-- | Returns the 'Path' associated to a given 'WidgetKey', if any. The search is+--   restricted to the parent _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 key, if any.+-- | Returns the 'WidgetId' associated to a given 'WidgetKey', if any. The+--   search is restricted to the parent _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 node info associated to a given path.-findWidgetByPath+-- | Returns the 'WidgetNodeInfo' associated to the given 'WidgetKey', if any.+--   The search is restricted to the parent _Composite_.+nodeInfoFromKey :: WidgetEnv s e -> WidgetKey -> Maybe WidgetNodeInfo+nodeInfoFromKey wenv key = path >>= nodeInfoFromPath wenv where+  path = pathFromKey wenv key++-- | Returns the 'WidgetId' associated to the given 'Path', if any.+widgetIdFromPath :: WidgetEnv s e -> Path -> Maybe WidgetId+widgetIdFromPath wenv path = mwni ^? _Just . L.widgetId where+  branch = wenv ^. L.findBranchByPath $ path+  mwni = Seq.lookup (length branch - 1) branch++{-# DEPRECATED findWidgetIdFromPath "Use 'widgetIdFromPath' instead." #-}+findWidgetIdFromPath :: WidgetEnv s e -> Path -> Maybe WidgetId+findWidgetIdFromPath = widgetIdFromPath++-- | Returns the 'WidgetNodeInfo' associated to the given 'Path', if any.+nodeInfoFromPath :: WidgetEnv s e -> Path -> Maybe WidgetNodeInfo+nodeInfoFromPath wenv path = mwni where+  branch = wenv ^. L.findBranchByPath $ path+  mwni = Seq.lookup (length branch - 1) branch++-- | Returns the 'WidgetNodeInfo' associated to a given 'Path'. The path will be+--   searched for starting from the provided 'WidgetNode'.+findChildNodeInfoByPath   :: WidgetEnv s e -> WidgetNode s e -> Path -> Maybe WidgetNodeInfo-findWidgetByPath wenv node target = mnode where+findChildNodeInfoByPath wenv node target = mnode where   branch = widgetFindBranchByPath (node ^. L.widget) wenv node target   mnode = case Seq.lookup (length branch - 1) branch of     Just child       | child ^. L.path == target -> Just child     _ -> Nothing --- | Returns the complete node info branch associated to a given path.-findWidgetBranchByPath+{-# DEPRECATED findWidgetByPath "Use 'findChildNodeInfoByPath' instead." #-}+findWidgetByPath+  :: WidgetEnv s e -> WidgetNode s e -> Path -> Maybe WidgetNodeInfo+findWidgetByPath = findChildNodeInfoByPath++-- | Returns the 'WidgetNodeInfo' branch associated to a given 'Path'. The path+--   will be searched for starting from the provided 'WidgetNode'.+findChildBranchByPath   :: WidgetEnv s e -> WidgetNode s e -> Path -> Seq WidgetNodeInfo-findWidgetBranchByPath wenv node target = branch where+findChildBranchByPath wenv node target = branch where   branch = widgetFindBranchByPath (node ^. L.widget) wenv node target +{-# DEPRECATED findWidgetBranchByPath "Use 'findChildBranchByPath' instead." #-}+findWidgetBranchByPath+  :: WidgetEnv s e -> WidgetNode s e -> Path -> Seq WidgetNodeInfo+findWidgetBranchByPath = findChildBranchByPath++-- | Returns the first parent 'WidgetNodeInfo' of the 'Path' that matches the+--   given 'WidgetType'.+findParentNodeInfoByType+  :: WidgetEnv s e -> Path -> WidgetType -> Maybe WidgetNodeInfo+findParentNodeInfoByType wenv path wtype = wniParent where+  isMatch wni = wni ^. L.widgetType == wtype+  branch = wenv ^. L.findBranchByPath $ path+  matches = Seq.filter isMatch branch+  wniParent = Seq.lookup (length matches - 1) matches+ -- | Helper functions that associates False to Vertical and True to Horizontal. getLayoutDirection :: Bool -> LayoutDirection getLayoutDirection False = LayoutVertical@@ -285,6 +332,14 @@ -- | Checks if the platform is macOS isMacOS :: WidgetEnv s e -> Bool isMacOS wenv = _weOs wenv == "Mac OS X"++{-|+Returns the current time in milliseconds. Adds appStartTs and timestamp fields+from 'WidgetEnv' and converts the result to the expected 'Integral' type.+-}+currentTimeMs :: Integral a => WidgetEnv s e -> a+currentTimeMs wenv = fromIntegral ts where+  ts = wenv ^. L.appStartTs + wenv ^. L.timestamp  -- | Returns a string description of a node and its children. widgetTreeDesc :: Int -> WidgetNode s e -> String
src/Monomer/Core/WidgetTypes.hs view
@@ -10,7 +10,9 @@ -} {-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE ExistentialQuantification #-}+{-# Language GeneralizedNewtypeDeriving #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE Strict #-} @@ -24,7 +26,9 @@ import Data.String (IsString(..)) import Data.Text (Text) import Data.Typeable (Typeable, typeOf)+import Data.Word (Word64) import GHC.Generics+import TextShow  import qualified Data.Text as T @@ -34,9 +38,18 @@ import Monomer.Event.Types import Monomer.Graphics.Types --- | Time ellapsed since startup-type Timestamp = Int+{-|+Time expressed in milliseconds. Useful for representing the time of events,+length of intervals, start time of the application and ellapsed time since its+start. +It can be converted from/to other numeric types using the standard functions.+-}+newtype Millisecond = Millisecond {+  unMilliseconds :: Word64+} deriving newtype (Eq, Ord, Enum, Bounded, Num, Real, Integral, Read, Show, Default, TextShow)+  deriving (Generic)+ -- | Type constraints for a valid model type WidgetModel s = Typeable s -- | Type constraints for a valid event@@ -94,8 +107,8 @@ requests (tasks, clipboard, etc). -} data WidgetId = WidgetId {-  _widTs :: Int,    -- ^ The timestamp when the instance was created.-  _widPath :: Path  -- ^ The path at creation time.+  _widTs :: Millisecond,  -- ^ The timestamp when the instance was created.+  _widPath :: Path      -- ^ The path at creation time. } deriving (Eq, Show, Ord, Generic)  instance Default WidgetId where@@ -139,7 +152,7 @@ {-| WidgetRequests are the way a widget can perform side effects, such as changing cursor icons, get/set the clipboard and perform asynchronous tasks. These-requests are included as part of a WidgetResult in different points in the+requests are included as part of a 'WidgetResult' in different points in the lifecycle of a widget. -} data WidgetRequest s e@@ -160,7 +173,7 @@   | SetFocus WidgetId   -- | Requests the clipboard contents. It will be received as a SystemEvent.   | GetClipboard WidgetId-  -- | Sets the clipboard to the given ClipboardData.+  -- | Sets the clipboard to the given 'ClipboardData'.   | SetClipboard ClipboardData   -- | Sets the viewport that should be remain visible when an on-screen   --   keyboard is displayed. Required for mobile.@@ -188,11 +201,11 @@   --   in order to reduce CPU usage. Widgets are responsible for requesting   --   rendering at points of interest. Mouse (except mouse move) and keyboard   --   events automatically generate render requests, but the result of a-  --   WidgetTask or WidgetProducer does not.+  --   'RunTask' or 'RunProducer' does not.   | RenderOnce   -- | Useful if a widget requires periodic rendering. An optional maximum   --   number of frames can be provided.-  | RenderEvery WidgetId Int (Maybe Int)+  | RenderEvery WidgetId Millisecond (Maybe Int)   -- | Stops a previous periodic rendering request.   | RenderStop WidgetId   {-|@@ -293,6 +306,8 @@   _weOs :: Text,   -- | Device pixel rate.   _weDpr :: Double,+  -- | The timestamp in milliseconds when the application started.+  _weAppStartTs :: Millisecond,   -- | Provides helper funtions for calculating text size.   _weFontManager :: FontManager,   -- | Returns the node info, and its parents', given a path from root.@@ -307,7 +322,11 @@   _weWindowSize :: Size,   -- | The active map of shared data.   _weWidgetShared :: MVar (Map Text WidgetShared),-  -- | The active map of WidgetKey -> WidgetNode, if any.+  {-+  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.+  -}   _weWidgetKeyMap :: WidgetKeyMap s e,   -- | The currently hovered path, if any.   _weHoveredPath :: Maybe Path,@@ -325,9 +344,12 @@   _weModel :: s,   -- | The input status, mainly mouse and keyboard.   _weInputStatus :: InputStatus,-  -- | The timestamp when this cycle started.-  _weTimestamp :: Timestamp,   {-|+  The timestamp in milliseconds when this event/message cycle started. This+  value starts from zero each time the application is run.+  -}+  _weTimestamp :: Millisecond,+  {-|   Whether the theme changed in this cycle. Should be considered when a widget   avoids merging as optimization, as the styles may have changed.   -}@@ -404,8 +426,8 @@ {-| An instance of the widget in the widget tree, without specific type information. This allows querying for widgets that may be nested in Composites, which are not-visible as a regular "WidgetNode" because of possible type mismatches (see-"WidgetKeyMap").+visible as a regular 'WidgetNode' because of possible type mismatches (see+'WidgetKeyMap'). -} data WidgetInstanceNode = WidgetInstanceNode {   -- | Information about the instance.
src/Monomer/Event/Core.hs view
@@ -39,6 +39,7 @@ isActionEvent SDL.TextInputEvent{} = True isActionEvent _ = False +-- | Configuration options for converting from an SDL event to a 'SystemEvent'. data ConvertEventsCfg = ConvertEventsCfg {   _cecOs :: Text,           -- ^ The host operating system.   _cecDpr :: Double,        -- ^ Device pixel rate.
src/Monomer/Event/Keyboard.hs view
@@ -46,6 +46,9 @@ keyUnknown :: KeyCode keyUnknown = getKeyCode SDL.KeycodeUnknown +keyEnter :: KeyCode+keyEnter = getKeyCode SDL.KeycodeReturn+ keyReturn :: KeyCode keyReturn = getKeyCode SDL.KeycodeReturn @@ -337,6 +340,56 @@ keyZ :: KeyCode keyZ = getKeyCode SDL.KeycodeZ +-- Key pad+keyPadDivide :: KeyCode+keyPadDivide = getKeyCode SDL.KeycodeKPDivide++keyPadMultiply :: KeyCode+keyPadMultiply = getKeyCode SDL.KeycodeKPMultiply++keyPadMinus :: KeyCode+keyPadMinus = getKeyCode SDL.KeycodeKPMinus++keyPadPlus :: KeyCode+keyPadPlus = getKeyCode SDL.KeycodeKPPlus++keyPadEnter :: KeyCode+keyPadEnter = getKeyCode SDL.KeycodeKPEnter++keyPadPeriod :: KeyCode+keyPadPeriod = getKeyCode SDL.KeycodeKPPeriod++-- Key pad numbers+keyPad0 :: KeyCode+keyPad0 = getKeyCode SDL.KeycodeKP0++keyPad1 :: KeyCode+keyPad1 = getKeyCode SDL.KeycodeKP1++keyPad2 :: KeyCode+keyPad2 = getKeyCode SDL.KeycodeKP2++keyPad3 :: KeyCode+keyPad3 = getKeyCode SDL.KeycodeKP3++keyPad4 :: KeyCode+keyPad4 = getKeyCode SDL.KeycodeKP4++keyPad5 :: KeyCode+keyPad5 = getKeyCode SDL.KeycodeKP5++keyPad6 :: KeyCode+keyPad6 = getKeyCode SDL.KeycodeKP6++keyPad7 :: KeyCode+keyPad7 = getKeyCode SDL.KeycodeKP7++keyPad8 :: KeyCode+keyPad8 = getKeyCode SDL.KeycodeKP8++keyPad9 :: KeyCode+keyPad9 = getKeyCode SDL.KeycodeKP9+ --  -- Mod keys@@ -657,3 +710,53 @@  isKeyZ :: KeyCode -> Bool isKeyZ = (== keyZ)++-- Key pad+isKeyPadDivide :: KeyCode -> Bool+isKeyPadDivide = (== keyPadDivide)++isKeyPadMultiply :: KeyCode -> Bool+isKeyPadMultiply = (== keyPadMultiply)++isKeyPadMinus :: KeyCode -> Bool+isKeyPadMinus = (== keyPadMinus)++isKeyPadPlus :: KeyCode -> Bool+isKeyPadPlus = (== keyPadPlus)++isKeyPadEnter :: KeyCode -> Bool+isKeyPadEnter = (== keyPadEnter)++isKeyPadPeriod :: KeyCode -> Bool+isKeyPadPeriod = (== keyPadPeriod)++-- Key pad numbers+isKeyPad0 :: KeyCode -> Bool+isKeyPad0 = (== keyPad0)++isKeyPad1 :: KeyCode -> Bool+isKeyPad1 = (== keyPad1)++isKeyPad2 :: KeyCode -> Bool+isKeyPad2 = (== keyPad2)++isKeyPad3 :: KeyCode -> Bool+isKeyPad3 = (== keyPad3)++isKeyPad4 :: KeyCode -> Bool+isKeyPad4 = (== keyPad4)++isKeyPad5 :: KeyCode -> Bool+isKeyPad5 = (== keyPad5)++isKeyPad6 :: KeyCode -> Bool+isKeyPad6 = (== keyPad6)++isKeyPad7 :: KeyCode -> Bool+isKeyPad7 = (== keyPad7)++isKeyPad8 :: KeyCode -> Bool+isKeyPad8 = (== keyPad8)++isKeyPad9 :: KeyCode -> Bool+isKeyPad9 = (== keyPad9)
src/Monomer/Graphics/FFI.chs view
@@ -37,10 +37,11 @@  #include "fontmanager.h" --- | Vector of 4 strict elements+-- | Vector of 4 strict elements. data V4 a = V4 !a !a !a !a   deriving (Show, Read, Eq, Ord) +-- | Bounds of a block of text. newtype Bounds   = Bounds (V4 CFloat)   deriving (Show, Read, Eq, Ord)@@ -62,6 +63,7 @@        pokeElemOff p' 2 c        pokeElemOff p' 3 d +-- | Position of a glyph in a text string. data GlyphPosition = GlyphPosition {   -- | Pointer of the glyph in the input string.   str :: !(Ptr CChar),@@ -98,9 +100,11 @@  {#pointer *FMGglyphPosition as GlyphPositionPtr -> GlyphPosition#} +-- | Reads Bounds from a pointer. peekBounds :: Ptr CFloat -> IO Bounds peekBounds = peek . castPtr +-- | Allocates space for Bounds. allocaBounds :: (Ptr CFloat -> IO b) -> IO b allocaBounds f = alloca (\(p :: Ptr Bounds) -> f (castPtr p)) @@ -121,6 +125,8 @@ {# fun unsafe fmInit {`Double'} -> `FMContext' #}  {# fun unsafe fmCreateFont {`FMContext', withCString*`Text', withCString*`Text'} -> `Int' #}++{# fun unsafe fmSetScale {`FMContext', `Double'} -> `()' #}  {# fun unsafe fmFontFace {`FMContext', withCString*`Text'} -> `()' #} 
src/Monomer/Graphics/FontManager.hs view
@@ -31,23 +31,26 @@  -- | Creates a font manager instance. makeFontManager-  :: [FontDef]    -- ^ The font definitions.-  -> Double       -- ^ The device pixel rate.+  :: [FontDef]       -- ^ The font definitions.+  -> Double          -- ^ The device pixel rate.   -> IO FontManager  -- ^ The created renderer. makeFontManager fonts dpr = do-  ctx <- fmInit 1 --dpr+  ctx <- fmInit dpr    validFonts <- foldM (loadFont ctx) [] fonts    when (null validFonts) $     putStrLn "Could not find any valid fonts. Text size calculations will fail."-  -  return $ newManager ctx dpr -newManager :: FMContext -> Double -> FontManager-newManager ctx dpr = FontManager {..} where-  computeTextMetrics font fontSize = unsafePerformIO $ do-    setFont ctx dpr font fontSize def+  return $ newManager ctx++newManager :: FMContext -> FontManager+newManager ctx = FontManager {..} where+  computeTextMetrics font fontSize =+    computeTextMetrics_ 1 font fontSize++  computeTextMetrics_ scale font fontSize = unsafePerformIO $ do+    setFont ctx scale font fontSize def     (asc, desc, lineh) <- fmTextMetrics ctx     lowerX <- Seq.lookup 0 <$> fmTextGlyphPositions ctx 0 0 "x"     let heightLowerX = case lowerX of@@ -55,24 +58,30 @@           Nothing -> realToFrac asc      return $ TextMetrics {-      _txmAsc = asc / dpr,-      _txmDesc = desc / dpr,-      _txmLineH = lineh / dpr,-      _txmLowerX = realToFrac heightLowerX / dpr+      _txmAsc = asc,+      _txmDesc = desc,+      _txmLineH = lineh,+      _txmLowerX = realToFrac heightLowerX     } -  computeTextSize font fontSize fontSpaceH text = unsafePerformIO $ do-    setFont ctx dpr font fontSize fontSpaceH+  computeTextSize font fontSize fontSpaceH text =+    computeTextSize_ 1 font fontSize fontSpaceH text++  computeTextSize_ scale font fontSize fontSpaceH text = unsafePerformIO $ do+    setFont ctx scale font fontSize fontSpaceH     (x1, y1, x2, y2) <- if text /= ""       then fmTextBounds ctx 0 0 text       else do         (asc, desc, lineh) <- fmTextMetrics ctx         return (0, 0, 0, lineh) -    return $ Size (realToFrac (x2 - x1) / dpr) (realToFrac (y2 - y1) / dpr)+    return $ Size (realToFrac (x2 - x1)) (realToFrac (y2 - y1)) -  computeGlyphsPos font fontSize fontSpaceH text = unsafePerformIO $ do-    setFont ctx dpr font fontSize fontSpaceH+  computeGlyphsPos font fontSize fontSpaceH text =+    computeGlyphsPos_ 1 font fontSize fontSpaceH text++  computeGlyphsPos_ scale font fontSize fontSpaceH text = unsafePerformIO $ do+    setFont ctx scale font fontSize fontSpaceH     glyphs <- if text /= ""       then fmTextGlyphPositions ctx 0 0 text       else return Seq.empty@@ -81,12 +90,13 @@     where       toGlyphPos chr glyph = GlyphPos {         _glpGlyph = chr,-        _glpXMin = realToFrac (glyphPosMinX glyph) / dpr,-        _glpXMax = realToFrac (glyphPosMaxX glyph) / dpr,-        _glpYMin = realToFrac (glyphPosMinY glyph) / dpr,-        _glpYMax = realToFrac (glyphPosMaxY glyph) / dpr,-        _glpW = realToFrac (glyphPosMaxX glyph - glyphPosMinX glyph) / dpr,-        _glpH = realToFrac (glyphPosMaxY glyph - glyphPosMinY glyph) / dpr+        _glpX = realToFrac (glyphX glyph),+        _glpXMin = realToFrac (glyphPosMinX glyph),+        _glpXMax = realToFrac (glyphPosMaxX glyph),+        _glpYMin = realToFrac (glyphPosMinY glyph),+        _glpYMax = realToFrac (glyphPosMaxY glyph),+        _glpW = realToFrac (glyphPosMaxX glyph - glyphPosMinX glyph),+        _glpH = realToFrac (glyphPosMaxY glyph - glyphPosMinY glyph)       }  loadFont :: FMContext -> [Text] -> FontDef -> IO [Text]@@ -97,7 +107,8 @@     else putStrLn ("Failed to load font: " ++ T.unpack name) >> return fonts  setFont :: FMContext -> Double -> Font -> FontSize -> FontSpace -> IO ()-setFont ctx dpr (Font name) (FontSize size) (FontSpace spaceH) = do+setFont ctx scale (Font name) (FontSize size) (FontSpace spaceH) = do+  fmSetScale ctx scale   fmFontFace ctx name-  fmFontSize ctx $ realToFrac $ size * dpr-  fmTextLetterSpacing ctx $ realToFrac $ spaceH * dpr+  fmFontSize ctx $ realToFrac size+  fmTextLetterSpacing ctx $ realToFrac spaceH
src/Monomer/Graphics/NanoVGRenderer.hs view
@@ -113,6 +113,10 @@  newRenderer :: VG.Context -> Double -> IORef Env -> Renderer newRenderer c rdpr envRef = Renderer {..} where+  {-+  rdpr is used to let nanovg know the real device pixel rate.+  dpr is set to 1 to disable all NanoVGRenderer internal calculations.+  -}   dpr = 1    beginFrame w h = do
src/Monomer/Graphics/RemixIcon.hs view
@@ -10,7 +10,7 @@ representative name. These code points can be used in labels or buttons to show icons instead of regular text. -Make sure to load the remixicon.ttf font in your application and set 'textFont'+Make sure to load the remixicon.ttf font in your application and set _textFont_ in the corresponding widget.  Existing icons can be browsed in https://remixicon.com.
src/Monomer/Graphics/Text.hs view
@@ -135,10 +135,11 @@   fSize = styleFontSize style   fSpcH = styleFontSpaceH style   fSpcV = styleFontSpaceV style+  break = styleTextLineBreak style   lineH = _txmLineH metrics    !metrics = computeTextMetrics fontMgr font fSize-  fitToWidth = fitLineToW fontMgr font fSize fSpcH fSpcV metrics+  fitToWidth = fitLineToW fontMgr font fSize fSpcH fSpcV metrics break    helper acc line = (cLines <> newLines, newTop) where     (cLines, cTop) = acc@@ -261,22 +262,25 @@   -> FontSpace   -> FontSpace   -> TextMetrics+  -> LineBreak   -> Double   -> Double   -> TextTrim   -> Text   -> Seq TextLine-fitLineToW fontMgr font fSize fSpcH fSpcV metrics top width trim text = res where+fitLineToW fontMgr font fSize fSpcH fSpcV metrics break top width trim text = res where   spaces = T.replicate 4 " "   newText = T.replace "\t" spaces text   !glyphs = computeGlyphsPos fontMgr font fSize fSpcH newText   -- Do not break line on trailing spaces, they are removed in the next step   -- In the case of KeepSpaces, lines with only spaces (empty looking) are valid   keepTailSpaces = trim == TrimSpaces-  groups = fitGroups (splitGroups glyphs) width keepTailSpaces+  groups+    | break == OnCharacters = splitGroups break width glyphs+    | otherwise = fitGroups (splitGroups break width glyphs) width keepTailSpaces   resetGroups-    | trim == TrimSpaces = fmap (resetGlyphs . trimGlyphs) groups-    | otherwise = fmap resetGlyphs groups+    | trim == TrimSpaces = fmap trimGlyphs groups+    | otherwise = groups   buildLine = buildTextLine font fSize fSpcH fSpcV metrics top   res     | text /= "" = Seq.mapWithIndex buildLine resetGroups@@ -413,26 +417,17 @@ isSpaceGroup Empty = False isSpaceGroup (g :<| gs) = isSpace (_glpGlyph g) -splitGroups :: Seq GlyphPos -> Seq GlyphGroup-splitGroups Empty = Empty-splitGroups glyphs = group <| splitGroups rest where+splitGroups :: LineBreak -> Double -> Seq GlyphPos -> Seq GlyphGroup+splitGroups _ _ Empty = Empty+splitGroups break width glyphs = group <| splitGroups break width rest where   g :<| gs = glyphs   groupWordFn = not . isWordDelimiter . _glpGlyph+  groupWidthFn g2 = _glpXMax g2 - _glpXMin g <= width+  atWord = break == OnSpaces   (group, rest)-    | isWordDelimiter (_glpGlyph g) = (Seq.singleton g, gs)-    | otherwise = Seq.spanl groupWordFn glyphs--resetGlyphs :: Seq GlyphPos -> Seq GlyphPos-resetGlyphs Empty = Empty-resetGlyphs gs@(g :<| _) = resetGlyphsPos gs (_glpXMin g)--resetGlyphsPos :: Seq GlyphPos -> Double -> Seq GlyphPos-resetGlyphsPos Empty _ = Empty-resetGlyphsPos (g :<| gs) offset = newG <| resetGlyphsPos gs offset where-  newG = g {-    _glpXMin = _glpXMin g - offset,-    _glpXMax = _glpXMax g - offset-  }+    | atWord && isWordDelimiter (_glpGlyph g) = (Seq.singleton g, gs)+    | atWord = Seq.spanl groupWordFn glyphs+    | otherwise = Seq.spanl groupWidthFn glyphs  trimGlyphs :: Seq GlyphPos -> Seq GlyphPos trimGlyphs glyphs = newGlyphs where
src/Monomer/Graphics/Types.hs view
@@ -140,6 +140,7 @@ -- | Information of a text glyph instance. data GlyphPos = GlyphPos {   _glpGlyph :: {-# UNPACK #-} !Char,   -- ^ The represented character.+  _glpX :: {-# UNPACK #-} !Double,     -- ^ The x coordinate used for rendering.   _glpXMin :: {-# UNPACK #-} !Double,  -- ^ The min x coordinate.   _glpXMax :: {-# UNPACK #-} !Double,  -- ^ The max x coordinate.   _glpYMin :: {-# UNPACK #-} !Double,  -- ^ The min x coordinate.@@ -151,6 +152,7 @@ instance Default GlyphPos where   def = GlyphPos {     _glpGlyph = ' ',+    _glpX = 0,     _glpXMin = 0,     _glpXMax = 0,     _glpYMin = 0,@@ -197,8 +199,8 @@ data TextLine = TextLine {   _tlFont :: !Font,              -- ^ The font name.   _tlFontSize :: !FontSize,      -- ^ The font size.-  _tlFontSpaceH :: !FontSpace, -- ^ The font spacing.-  _tlFontSpaceV :: !FontSpace, -- ^ The vertical line spacing.+  _tlFontSpaceH :: !FontSpace,   -- ^ The font spacing.+  _tlFontSpaceV :: !FontSpace,   -- ^ The vertical line spacing.   _tlMetrics :: !TextMetrics,    -- ^ The text metrics for the given font/size.   _tlText :: !Text,              -- ^ The represented text.   _tlSize :: !Size,              -- ^ The size the formatted text takes.@@ -228,20 +230,39 @@  -- | The definition of a loaded image. data ImageDef = ImageDef {-  _idfName :: Text,            -- ^ The logic name of the image.+  _idfName :: Text,              -- ^ The logic name of the image.   _idfSize :: Size,              -- ^ The dimensions of the image.   _idfImgData :: BS.ByteString,  -- ^ The image data as RGBA 4-bytes blocks.   _idfFlags :: [ImageFlag]       -- ^ The image flags. } deriving (Eq, Show, Generic) --- | Text metrics related functions.+{-|+Text metrics related functions.++Two different versions of each function exist:++- Default one, without underscore, does not apply scaling.+- Version with a trailing underscore, that receives an extra scale argument.++In case the text is going to be rendered with a scale factor applied on+'Renderer' (by calling 'setScale'), it is recommended to apply the scale here+too (otherwise there will be differences in size and positioning). In most use+cases these functions will never be called, preferring the non underscore+versions.+-} data FontManager = FontManager {   -- | Returns the text metrics of a given font and size.   computeTextMetrics :: Font -> FontSize -> TextMetrics,+  -- | Returns the text metrics of a given font and size, applying scale.+  computeTextMetrics_ :: Double -> Font -> FontSize -> TextMetrics,   -- | Returns the size of the line of text given font and size.   computeTextSize :: Font -> FontSize -> FontSpace -> Text -> Size,+  -- | Returns the size of the line of text given font and size, applying scale.+  computeTextSize_ :: Double -> Font -> FontSize -> FontSpace -> Text -> Size,   -- | Returns the glyphs of the line of text given font and size.-  computeGlyphsPos :: Font -> FontSize -> FontSpace -> Text -> Seq GlyphPos+  computeGlyphsPos :: Font -> FontSize -> FontSpace -> Text -> Seq GlyphPos,+  -- | Returns the glyphs of the line of text given font and size, applying scale.+  computeGlyphsPos_ :: Double -> Font -> FontSize -> FontSpace -> Text -> Seq GlyphPos }  -- | Low level rendering definitions.
src/Monomer/Helper.hs view
@@ -9,6 +9,8 @@ Helper functions used across the library. They do not belong to any specific module and are not directly exported. -}+{-# LANGUAGE BangPatterns #-}+ module Monomer.Helper where  import Control.Exception (SomeException, catch)@@ -20,7 +22,8 @@ -- | Concats a list of Monoids or returns Nothing if empty. maybeConcat :: Monoid a => [a] -> Maybe a maybeConcat [] = Nothing-maybeConcat lst = Just (mconcat lst)+maybeConcat lst = Just merged where+  !merged = mconcat lst  -- | Runs an action until Nothing is returned, collecting the results in a list. collectJustM :: MonadIO m => m (Maybe a) -> m [a]@@ -68,3 +71,8 @@ -- | Catches any exception thrown by the provided action catchAny :: IO a -> (SomeException -> IO a) -> IO a catchAny = catch++-- | Returns Just the first item if the list is not empty, Nothing otherwise.+headMay :: [a] -> Maybe a+headMay [] = Nothing+headMay (x : _) = Just x
src/Monomer/Main/Core.hs view
@@ -19,11 +19,11 @@   startApp ) where -import Control.Concurrent (MVar, forkIO, forkOS, newMVar, threadDelay)+import Control.Concurrent import Control.Concurrent.STM.TChan (TChan, newTChanIO, readTChan, writeTChan)+import Control.Exception import Control.Lens ((&), (^.), (.=), (.~), use) import Control.Monad (unless, void, when)-import Control.Monad.Catch import Control.Monad.Extra import Control.Monad.State import Control.Monad.STM (atomically)@@ -32,9 +32,12 @@ import Data.Map (Map) import Data.List (foldl') import Data.Text (Text)+import Data.Time+import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)+import Graphics.GL  import qualified Data.Map as Map-import qualified Graphics.Rendering.OpenGL as GL+import qualified Data.Text as T import qualified SDL import qualified Data.Sequence as Seq @@ -71,18 +74,16 @@  data MainLoopArgs sp e ep = MainLoopArgs {   _mlOS :: Text,-  _mlRenderer :: Maybe Renderer,   _mlTheme :: Theme,-  _mlAppStartTs :: Int,+  _mlAppStartTs :: Millisecond,   _mlMaxFps :: Int,-  _mlLatestRenderTs :: Int,-  _mlFrameStartTs :: Int,-  _mlFrameAccumTs :: Int,+  _mlLatestRenderTs :: Millisecond,+  _mlFrameStartTs :: Millisecond,+  _mlFrameAccumTs :: Millisecond,   _mlFrameCount :: Int,   _mlExitEvents :: [e],   _mlWidgetRoot :: WidgetNode sp ep,-  _mlWidgetShared :: MVar (Map Text WidgetShared),-  _mlChannel :: TChan (RenderMsg sp ep)+  _mlWidgetShared :: MVar (Map Text WidgetShared) }  data RenderState s e = RenderState {@@ -134,7 +135,8 @@   dpr <- use L.dpr   winSize <- use L.windowSize -  let useRenderThread = fromMaybe True (_apcUseRenderThread config)+  let useRenderThreadFlag = fromMaybe True (_apcUseRenderThread config)+  let useRenderThread = useRenderThreadFlag && rtsSupportsBoundThreads   let maxFps = fromMaybe 60 (_apcMaxFps config)   let fonts = _apcFonts config   let theme = fromMaybe def (_apcTheme config)@@ -142,18 +144,16 @@   let mainBtn = fromMaybe BtnLeft (_apcMainButton config)   let contextBtn = fromMaybe BtnRight (_apcContextButton config) -  startTs <- fmap fromIntegral SDL.ticks+  appStartTs <- getCurrentTimestamp   model <- use L.mainModel   os <- liftIO getPlatform   widgetSharedMVar <- liftIO $ newMVar Map.empty-  renderer <- if useRenderThread-    then return Nothing-    else liftIO $ Just <$> makeRenderer fonts dpr   fontManager <- liftIO $ makeFontManager fonts dpr    let wenv = WidgetEnv {     _weOs = os,     _weDpr = dpr,+    _weAppStartTs = appStartTs,     _weFontManager = fontManager,     _weFindBranchByPath = const Seq.empty,     _weMainButton = mainBtn,@@ -170,7 +170,7 @@     _weMainBtnPress = Nothing,     _weModel = model,     _weInputStatus = def,-    _weTimestamp = startTs,+    _weTimestamp = 0,     _weThemeChanged = False,     _weInTopLayer = const True,     _weLayoutDirection = LayoutNone,@@ -180,33 +180,66 @@   let pathReadyRoot = widgetRoot         & L.info . L.path .~ rootPath         & L.info . L.widgetId .~ WidgetId (wenv ^. L.timestamp) rootPath+  let makeMainThreadRenderer = do+        renderer <- liftIO $ makeRenderer fonts dpr+        L.renderMethod .= Left renderer+        return RenderSetupSingle +  setupRes <- if useRenderThread+    then do+      stpChan <- liftIO newTChanIO++      liftIO . void . forkOS $+        {-+        The wenv and widgetRoot values are not used, since they are replaced+        during MsgInit. Kept to avoid issues with the Strict pragma.+        -}+        startRenderThread stpChan channel window glCtx fonts dpr wenv widgetRoot++      setupRes <- liftIO . atomically $ readTChan stpChan++      case setupRes of+        RenderSetupMakeCurrentFailed msg -> do+          liftIO . putStrLn $ "Setup of the rendering thread failed: " ++ msg+          liftIO . putStrLn $ "Falling back to rendering in the main thread. "+            ++ "The content may not be updated while resizing the window."++          makeMainThreadRenderer+        _ -> do+          return RenderSetupMulti+    else do+      makeMainThreadRenderer+   handleResourcesInit   (newWenv, newRoot, _) <- handleWidgetInit wenv pathReadyRoot +  {-+  Deferred initialization step to account for Widgets that rely on OpenGL. They+  need the Renderer to be setup before handleWidgetInit is called, and it is+  safer to initialize the watcher after this happens.+  -}+  case setupRes of+    RenderSetupMulti -> do+      liftIO . atomically $ writeTChan channel (MsgInit newWenv newRoot)+      liftIO $ watchWindowResize channel+    _ -> return ()+   let loopArgs = MainLoopArgs {     _mlOS = os,-    _mlRenderer = renderer,     _mlTheme = theme,     _mlMaxFps = maxFps,-    _mlAppStartTs = 0,+    _mlAppStartTs = appStartTs,     _mlLatestRenderTs = 0,-    _mlFrameStartTs = startTs,+    _mlFrameStartTs = 0,     _mlFrameAccumTs = 0,     _mlFrameCount = 0,     _mlExitEvents = exitEvents,     _mlWidgetRoot = newRoot,-    _mlWidgetShared = widgetSharedMVar,-    _mlChannel = channel+    _mlWidgetShared = widgetSharedMVar   }    L.mainModel .= _weModel newWenv -  when useRenderThread $ do-    liftIO $ watchWindowResize channel-    liftIO . void . forkOS $-      startRenderThread channel window glCtx fonts dpr newWenv newRoot-   mainLoop window fontManager config loopArgs  mainLoop@@ -219,7 +252,7 @@ mainLoop window fontManager config loopArgs = do   let MainLoopArgs{..} = loopArgs -  startTicks <- fmap fromIntegral SDL.ticks+  startTs <- getEllapsedTimestampSince _mlAppStartTs   events <- SDL.pumpEvents >> SDL.pollEvents    windowSize <- use L.windowSize@@ -237,7 +270,7 @@   currWinSize <- liftIO $ getViewportSize window dpr    let Size rw rh = windowSize-  let ts = startTicks - _mlFrameStartTs+  let ts = startTs - _mlFrameStartTs   let eventsPayload = fmap SDL.eventPayload events   let quit = SDL.QuitEvent `elem` eventsPayload @@ -264,8 +297,9 @@   let wenv = WidgetEnv {     _weOs = _mlOS,     _weDpr = dpr,+    _weAppStartTs = _mlAppStartTs,     _weFontManager = fontManager,-    _weFindBranchByPath = findWidgetBranchByPath wenv _mlWidgetRoot,+    _weFindBranchByPath = findChildBranchByPath wenv _mlWidgetRoot,     _weMainButton = mainBtn,     _weContextButton = contextBtn,     _weTheme = _mlTheme,@@ -280,7 +314,7 @@     _weMainBtnPress = mainPress,     _weModel = currentModel,     _weInputStatus = inputStatus,-    _weTimestamp = startTicks,+    _weTimestamp = startTs,     _weThemeChanged = False,     _weInTopLayer = const True,     _weLayoutDirection = LayoutNone,@@ -305,37 +339,38 @@       handleResizeWidgets (seWenv, seRoot, Seq.empty)     else return (seWenv, seRoot, Seq.empty) -  endTicks <- fmap fromIntegral SDL.ticks+  endTs <- getEllapsedTimestampSince _mlAppStartTs    -- Rendering-  renderCurrentReq <- checkRenderCurrent startTicks _mlLatestRenderTs+  renderCurrentReq <- checkRenderCurrent startTs _mlLatestRenderTs -  let useRenderThread = fromMaybe True (_apcUseRenderThread config)   let renderEvent = any isActionEvent eventsPayload   let winRedrawEvt = windowResized || windowExposed   let renderNeeded = winRedrawEvt || renderEvent || renderCurrentReq -  when (renderNeeded && useRenderThread) $-    liftIO . atomically $ writeTChan _mlChannel (MsgRender newWenv newRoot)+  when renderNeeded $ do+    renderMethod <- use L.renderMethod -  when (renderNeeded && not useRenderThread) $ do-    let renderer = fromJust _mlRenderer-    let bgColor = newWenv ^. L.theme . L.clearColor+    case renderMethod of+      Right renderChan -> do+        liftIO . atomically $ writeTChan renderChan (MsgRender newWenv newRoot)+      Left renderer -> do+        let bgColor = newWenv ^. L.theme . L.clearColor -    liftIO $ renderWidgets window dpr renderer bgColor newWenv newRoot+        liftIO $ renderWidgets window dpr renderer bgColor newWenv newRoot +  -- Used in the next rendering cycle   L.renderRequested .= windowResized    let fps = realToFrac _mlMaxFps   let frameLength = round (1000000 / fps)-  let remainingMs = endTicks - startTicks-  let tempDelay = abs (frameLength - remainingMs * 1000)+  let remainingMs = endTs - startTs+  let tempDelay = abs (frameLength - fromIntegral remainingMs * 1000)   let nextFrameDelay = min frameLength tempDelay-  let latestRenderTs = if renderNeeded then startTicks else _mlLatestRenderTs+  let latestRenderTs = if renderNeeded then startTs else _mlLatestRenderTs   let newLoopArgs = loopArgs {-    _mlAppStartTs = _mlAppStartTs + ts,     _mlLatestRenderTs = latestRenderTs,-    _mlFrameStartTs = startTicks,+    _mlFrameStartTs = startTs,     _mlFrameAccumTs = if newSecond then 0 else _mlFrameAccumTs + ts,     _mlFrameCount = if newSecond then 0 else _mlFrameCount + 1,     _mlWidgetRoot = newRoot@@ -350,9 +385,18 @@    unless shouldQuit (mainLoop window fontManager config newLoopArgs) +{-+Attempts to initialize a GL context in a separate OS thread to handle rendering+actions. This allows for continuous content updates when the user resizes the+window.++In case the setup fails, it notifies the parent process so it can fall back to+rendering in the main thread.+-} startRenderThread   :: (Eq s, WidgetEvent e)-  => TChan (RenderMsg s e)+  => TChan RenderSetupResult+  -> TChan (RenderMsg s e)   -> SDL.Window   -> SDL.GLContext   -> [FontDef]@@ -360,12 +404,23 @@   -> WidgetEnv s e   -> WidgetNode s e   -> IO ()-startRenderThread channel window glCtx fonts dpr wenv root = do-  SDL.glMakeCurrent window glCtx-  renderer <- liftIO $ makeRenderer fonts dpr-  fontMgr <- liftIO $ makeFontManager fonts dpr+startRenderThread setupChan msgChan window glCtx fonts dpr wenv root = do+  resp <- try $ SDL.glMakeCurrent window glCtx -  waitRenderMsg channel window renderer fontMgr state+  case resp of+    Right{} -> do+      renderer <- liftIO $ makeRenderer fonts dpr+      fontMgr <- liftIO $ makeFontManager fonts dpr++      atomically $ writeTChan setupChan RenderSetupMulti++      waitRenderMsg msgChan window renderer fontMgr state+    Left (SDL.SDLCallFailed _ _ err) -> do+      let msg = T.unpack err+      atomically $ writeTChan setupChan (RenderSetupMakeCurrentFailed msg)+    Left e -> do+      let msg = displayException e+      atomically $ writeTChan setupChan (RenderSetupMakeCurrentFailed msg)   where     state = RenderState dpr wenv root @@ -377,10 +432,10 @@   -> FontManager   -> RenderState s e   -> IO ()-waitRenderMsg channel window renderer fontMgr state = do-  msg <- liftIO . atomically $ readTChan channel+waitRenderMsg msgChan window renderer fontMgr state = do+  msg <- atomically $ readTChan msgChan   newState <- handleRenderMsg window renderer fontMgr state msg-  waitRenderMsg channel window renderer fontMgr newState+  waitRenderMsg msgChan window renderer fontMgr newState  handleRenderMsg   :: (Eq s, WidgetEvent e)@@ -390,6 +445,9 @@   -> RenderState s e   -> RenderMsg s e   -> IO (RenderState s e)+handleRenderMsg window renderer fontMgr state (MsgInit newWenv newRoot) = do+  let RenderState dpr _ _ = state+  return (RenderState dpr newWenv newRoot) handleRenderMsg window renderer fontMgr state (MsgRender tmpWenv newRoot) = do   let RenderState dpr _ _ = state   let newWenv = tmpWenv@@ -434,13 +492,10 @@   Size dwW dwH <- getDrawableSize window   Size vpW vpH <- getViewportSize window dpr -  let position = GL.Position 0 0-  let size = GL.Size (round dwW) (round dwH)--  GL.viewport GL.$= (position, size)+  glViewport 0 0 (round dwW) (round dwH) -  GL.clearColor GL.$= clearColor4-  GL.clear [GL.ColorBuffer]+  glClearColor r g b a+  glClear GL_COLOR_BUFFER_BIT    beginFrame renderer vpW vpH   widgetRender (widgetRoot ^. L.widget) wenv widgetRoot renderer@@ -459,8 +514,7 @@     r = fromIntegral (clearColor ^. L.r) / 255     g = fromIntegral (clearColor ^. L.g) / 255     b = fromIntegral (clearColor ^. L.b) / 255-    a = clearColor ^. L.a-    clearColor4 = GL.Color4 r g b (realToFrac a)+    a = realToFrac (clearColor ^. L.a)  watchWindowResize :: TChan (RenderMsg s e) -> IO () watchWindowResize channel = do@@ -473,7 +527,7 @@         atomically $ writeTChan channel (MsgResize newSize)       _ -> return () -checkRenderCurrent :: (MonomerM s e m) => Int -> Int -> m Bool+checkRenderCurrent :: (MonomerM s e m) => Millisecond -> Millisecond -> m Bool checkRenderCurrent currTs renderTs = do   renderCurrent <- use L.renderRequested   schedule <- use L.renderSchedule@@ -483,14 +537,14 @@     requiresRender = renderScheduleReq currTs renderTs     renderNext schedule = any requiresRender schedule -renderScheduleReq :: Int -> Int -> RenderSchedule -> Bool+renderScheduleReq :: Millisecond -> Millisecond -> RenderSchedule -> Bool renderScheduleReq currTs renderTs schedule = required where   RenderSchedule _ start ms _ = schedule   stepCount = floor (fromIntegral (currTs - start) / fromIntegral ms)   stepTs = start + ms * stepCount   required = renderTs < stepTs -renderScheduleActive :: Int -> RenderSchedule -> Bool+renderScheduleActive :: Millisecond -> RenderSchedule -> Bool renderScheduleActive currTs schedule = scheduleActive where   RenderSchedule _ start ms count = schedule   stepCount = floor (fromIntegral (currTs - start) / fromIntegral ms)@@ -507,3 +561,13 @@ isMouseEntered :: [SDL.EventPayload] -> Bool isMouseEntered eventsPayload = not status where   status = null [ e | e@SDL.WindowGainedMouseFocusEvent {} <- eventsPayload ]++getCurrentTimestamp :: MonadIO m => m Millisecond+getCurrentTimestamp = toMs <$> liftIO getCurrentTime+  where+    toMs = floor . (1e3 *) . nominalDiffTimeToSeconds . utcTimeToPOSIXSeconds++getEllapsedTimestampSince :: MonadIO m => Millisecond -> m Millisecond+getEllapsedTimestampSince start = do+  ts <- getCurrentTimestamp+  return (ts - start)
src/Monomer/Main/Handlers.hs view
@@ -39,7 +39,6 @@ import Data.Sequence (Seq(..), (|>)) import Data.Text (Text) import Data.Typeable (Typeable, typeOf)-import Safe (headMay) import SDL (($=))  import qualified Data.Map as Map@@ -52,7 +51,8 @@  import Monomer.Core import Monomer.Event-import Monomer.Helper (seqStartsWith)+import Monomer.Graphics+import Monomer.Helper (headMay, seqStartsWith) import Monomer.Main.Types import Monomer.Main.Util @@ -89,7 +89,7 @@     let (curWenv, curRoot, curReqs) = curStep     let target = fromMaybe focused evtTarget     let curWidget = curRoot ^. L.widget-    let targetWni = evtTarget >>= findWidgetByPath curWenv curRoot+    let targetWni = evtTarget >>= findChildNodeInfoByPath curWenv curRoot     let targetWid = (^. L.widgetId) <$> targetWni      when (isOnEnter evt) $@@ -108,7 +108,7 @@           & L.hoveredPath .~ hoveredPath           & L.mainBtnPress .~ mainBtnPress           & L.inputStatus .~ inputStatus-    let findBranchByPath path = findWidgetBranchByPath tmpWenv curRoot path+    let findBranchByPath path = findChildBranchByPath tmpWenv curRoot path     let newWenv = tmpWenv           & L.findBranchByPath .~ findBranchByPath     (wenv2, root2, reqs2) <- handleSystemEvent newWenv curRoot evt target@@ -260,7 +260,7 @@   -> m (HandlerStep s e)  -- ^ Updated state/"HandlerStep". handleResizeWidgets previousStep = do   windowSize <- use L.windowSize-  resizeCheckFn <- makeResizeChechFn+  resizeCheckFn <- makeResizeCheckFn    let viewport = Rect 0 0 (windowSize ^. L.w) (windowSize ^. L.h)   let (wenv, root, reqs) = previousStep@@ -277,7 +277,7 @@    return (wenv2, root2, reqs <> reqs2)   where-    makeResizeChechFn = do+    makeResizeCheckFn = do       resizeRequests <- use L.resizeRequests       paths <- mapM getWidgetIdPath resizeRequests       let parts = Set.fromDistinctAscList . drop 1 . toList . Seq.inits@@ -518,7 +518,7 @@ handleRenderEvery   :: MonomerM s e m   => WidgetId-  -> Int+  -> Millisecond   -> Maybe Int   -> HandlerStep s e   -> m (HandlerStep s e)@@ -548,9 +548,12 @@ handleRemoveRendererImage   :: MonomerM s e m => Text -> HandlerStep s e -> m (HandlerStep s e) handleRemoveRendererImage name previousStep = do-  renderChannel <- use L.renderChannel+  renderMethod <- use L.renderMethod -  liftIO . atomically $ writeTChan renderChannel (MsgRemoveImage name)+  case renderMethod of+    Left renderer -> liftIO $ deleteImage renderer name+    Right chan -> liftIO . atomically $ writeTChan chan (MsgRemoveImage name)+   return previousStep  handleExitApplication@@ -662,9 +665,17 @@   -> HandlerStep s e   -> m (HandlerStep s e) handleRunInRenderThread widgetId path handler previousStep = do-  renderChannel <- use L.renderChannel+  renderMethod <- use L.renderMethod -  handleRunTask widgetId path (taskWrapper renderChannel) previousStep+  task <- case renderMethod of+    Left renderer -> do+      -- Force running in main thread to avoid issues with OpenGL+      result <- liftIO handler+      return (return result)+    Right chan -> do+      return $ liftIO (taskWrapper chan)++  handleRunTask widgetId path task previousStep   where     taskWrapper renderChannel = do       msgChan <- newTChanIO@@ -829,7 +840,7 @@   restartPath = fromMaybe emptyPath overlay   candidateWni = widgetFindNextFocus widget wenv widgetRoot dir start   fromRootWni = widgetFindNextFocus widget wenv widgetRoot dir restartPath-  focusWni = fromMaybe def (findWidgetByPath wenv widgetRoot start)+  focusWni = fromMaybe def (findChildNodeInfoByPath wenv widgetRoot start)   nextFocus = candidateWni <|> fromRootWni <|> Just focusWni  dropNonParentWidgetId
src/Monomer/Main/Platform.hs view
@@ -22,6 +22,7 @@   getDisplayDPI ) where +import Control.Exception (finally) import Control.Monad (void) import Control.Monad.Extra (whenJust) import Control.Monad.State@@ -38,6 +39,8 @@ import qualified SDL.Input.Mouse as Mouse import qualified SDL.Raw as Raw import qualified SDL.Raw.Error as SRE+import qualified SDL.Internal.Types as SIT+import qualified SDL.Video.Renderer as SVR  import Monomer.Common import Monomer.Core.StyleTypes@@ -65,10 +68,15 @@    platform <- getPlatform   initDpiAwareness-  factor <- case platform of++  baseFactor <- case platform of     "Windows" -> getWindowsFactor     "Linux" -> getLinuxFactor     _ -> return 1 -- macOS++  let factor+        | disableAutoScale = 1+        | otherwise = baseFactor   let (winW, winH) = (factor * fromIntegral baseW, factor * fromIntegral baseH)    window <-@@ -91,6 +99,8 @@         | platform `elem` ["Windows", "Linux"] = (scaleFactor, 1 / scaleFactor)         | otherwise = (scaleFactor * contentRatio, 1 / scaleFactor) -- macOS +  setWindowIcon window config+   whenJust (_apcWindowTitle config) $ \title ->     SDL.windowTitle window $= title @@ -123,6 +133,7 @@     }     compositingFlag = fromMaybe False (_apcDisableCompositing config)     userScaleFactor = fromMaybe 1 (_apcScaleFactor config)+    disableAutoScale = _apcDisableAutoScale config == Just True     (baseW, baseH) = case _apcWindowState config of       Just (MainWindowNormal size) -> size       _ -> defaultWindowSize@@ -135,6 +146,16 @@       Just MainWindowMaximized -> True       _ -> False +setWindowIcon :: SDL.Window -> AppConfig e -> IO ()+setWindowIcon (SIT.Window winPtr) config =+  forM_ (_apcWindowIcon config) $ \iconPath -> do+    iconSurface <- SVR.loadBMP (T.unpack iconPath)+    let SVR.Surface iconSurfacePtr _ = iconSurface+    finally+      -- Note: this can use the high-level setWindowIcon once it is available (https://github.com/haskell-game/sdl2/pull/243)+      (Raw.setWindowIcon winPtr iconSurfacePtr)+      (SVR.freeSurface iconSurface)+ -- | Destroys the provided window, shutdowns the video subsystem and SDL. detroySDLWindow :: SDL.Window -> IO () detroySDLWindow window = do@@ -194,28 +215,32 @@         vdpi <- peek pvdpi         return (realToFrac ddpi, realToFrac hdpi, realToFrac vdpi) --- | Returns the default resize factor for Windows+-- | Returns the default resize factor for Windows. getWindowsFactor :: IO Double-getWindowsFactor = do-  (ddpi, hdpi, vdpi) <- getDisplayDPI-  return (hdpi / 96)+getWindowsFactor = max 1 <$> getDisplayDPIFactor -{-|-Returns a resizing factor to handle HiDPI on Linux. Currently only tested on-Wayland (Ubuntu 21.04).--}+-- | Returns the default resize factor for Linux. getLinuxFactor :: IO Double-getLinuxFactor =+getLinuxFactor = do+  dpiFactor <- getDisplayDPIFactor+   alloca $ \pmode -> do     Raw.getCurrentDisplayMode 0 pmode     mode <- peek pmode+     let width = Raw.displayModeW mode-    -- Applies scale in half steps (1, 1.5, 2, etc)-    let baseFactor = 2 * fromIntegral width / 1920+    let detectedDPI+          | dpiFactor > 0 = dpiFactor+          | width <= 1920 = 1+          | otherwise = 2 -    if width <= 1920-      then return 1-      else return (fromIntegral (ceiling baseFactor) / 2)+    return detectedDPI++-- | Returns DPI scaling factor using SDL_GetDisplayDPI.+getDisplayDPIFactor :: IO Double+getDisplayDPIFactor = do+  (ddpi, hdpi, vdpi) <- getDisplayDPI+  return (hdpi / 96)  setDisableCompositorHint :: Bool -> IO () setDisableCompositorHint disable = void $
src/Monomer/Main/Types.hs view
@@ -47,11 +47,19 @@  -- | Messages received by the rendering thread. data RenderMsg s e-  = MsgRender (WidgetEnv s e) (WidgetNode s e)+  = MsgInit (WidgetEnv s e) (WidgetNode s e)+  | MsgRender (WidgetEnv s e) (WidgetNode s e)   | MsgResize Size   | MsgRemoveImage Text   | forall i . MsgRunInRender (TChan i) (IO i) +-- | Result from attempting to set up the secondary rendering thread.+data RenderSetupResult+  = RenderSetupSingle+  | RenderSetupMulti+  | RenderSetupMakeCurrentFailed String+  deriving (Eq, Show)+ {-| Requirements for periodic rendering by a widget. Start time is stored to calculate next frame based on the step ms. A maximum number of repetitions may@@ -59,8 +67,8 @@ -} data RenderSchedule = RenderSchedule {   _rsWidgetId :: WidgetId,-  _rsStart :: Int,-  _rsMs :: Int,+  _rsStart :: Millisecond,+  _rsMs :: Millisecond,   _rsRepeat :: Maybe Int } deriving (Eq, Show, Generic) @@ -93,8 +101,8 @@   _mcDpr :: Double,   -- | Event pixel rate.   _mcEpr :: Double,-  -- | Event pixel rate.-  _mcRenderChannel :: TChan (RenderMsg s e),+  -- | Renderer instance or communication channel with the render thread.+  _mcRenderMethod :: Either Renderer (TChan (RenderMsg s e)),   -- | Input status (mouse and keyboard).   _mcInputStatus :: InputStatus,   -- | Cursor icons (a stack is used because of parent -> child relationship).@@ -153,6 +161,8 @@   _apcWindowResizable :: Maybe Bool,   -- | Whether the main window has a border.   _apcWindowBorder :: Maybe Bool,+  -- | Path to an icon file in BMP format.+  _apcWindowIcon :: Maybe Text,   -- | Whether a separate render thread should be used. Defaults to True.   _apcUseRenderThread :: Maybe Bool,   {-|@@ -163,11 +173,15 @@   _apcMaxFps :: Maybe Int,   {-|   Scale factor to apply. This factor only affects the content, not the size of-  the window. It is applied in addition to the OS zoom in plaforms where it is-  reliably detected (i.e., system scaling may not be detected reliably on Linux)+  the window. It is applied in addition to the detected display scaling.   -}   _apcScaleFactor :: Maybe Double,   {-|+  Whether display scaling detection should not be attempted. If set to True, the+  display scale will be set to 1. This works together with 'appScaleFactor'.+  -}+  _apcDisableAutoScale :: Maybe Bool,+  {-|   Available fonts to the application. An empty list will make it impossible to   render text.   -}@@ -200,9 +214,11 @@     _apcWindowTitle = Nothing,     _apcWindowResizable = Nothing,     _apcWindowBorder = Nothing,+    _apcWindowIcon = Nothing,     _apcUseRenderThread = Nothing,     _apcMaxFps = Nothing,     _apcScaleFactor = Nothing,+    _apcDisableAutoScale = Nothing,     _apcFonts = [],     _apcTheme = Nothing,     _apcInitEvent = [],@@ -222,9 +238,11 @@     _apcWindowTitle = _apcWindowTitle a2 <|> _apcWindowTitle a1,     _apcWindowResizable = _apcWindowResizable a2 <|> _apcWindowResizable a1,     _apcWindowBorder = _apcWindowBorder a2 <|> _apcWindowBorder a1,+    _apcWindowIcon = _apcWindowIcon a2 <|> _apcWindowIcon a1,     _apcUseRenderThread = _apcUseRenderThread a2 <|> _apcUseRenderThread a1,     _apcMaxFps = _apcMaxFps a2 <|> _apcMaxFps a1,     _apcScaleFactor = _apcScaleFactor a2 <|> _apcScaleFactor a1,+    _apcDisableAutoScale = _apcDisableAutoScale a2 <|> _apcDisableAutoScale a1,     _apcFonts = _apcFonts a1 ++ _apcFonts a2,     _apcTheme = _apcTheme a2 <|> _apcTheme a1,     _apcInitEvent = _apcInitEvent a1 ++ _apcInitEvent a2,@@ -265,16 +283,32 @@   _apcWindowBorder = Just border } +-- | Path to an icon file in BMP format.+appWindowIcon :: Text -> AppConfig e+appWindowIcon path = def {+  _apcWindowIcon = Just path+}+ {-| Performs rendering on the main thread. On macOS and Windows this also disables continuous rendering on window resize, but in some Linux configurations it still works. -This option is useful when OpenGL driver issues prevent normal startup showing-the "Unable to make GL context current" error.+This configuration option was originally available to handle: -It can also be used for single threaded applications (without -threaded).+  - OpenGL driver issues which prevented normal startup showing the "Unable to+    make GL context current" error.+  - Single threaded applications (without -threaded) which cannot use forkOS.++This flag is no longer necessary for those cases, since the library will:++  - Attempt to fall back to rendering on the main thread if setting up a+    secondary rendering thread fails.+  - Will not attempt to set up a secondary rendering thread if the runtime does+    not support bound threads (i.e. compiled without the -threaded flag). -}+{-# DEPRECATED appRenderOnMainThread+  "Should no longer be needed. Check appRenderOnMainThread's Haddock page." #-} appRenderOnMainThread :: AppConfig e appRenderOnMainThread = def {   _apcUseRenderThread = Just False@@ -291,13 +325,56 @@ }  {-|-Scale factor to apply. This factor only affects the content, not the size of the-window. It is applied in addition to the OS zoom in plaforms where it is-reliably detected (i.e., system scaling may not be detected reliably on Linux).+Scale factor to apply to the viewport. This factor only affects the content, not+the size of the window. It is applied in addition to the detected display scale+factor, and can be useful if the detected value is not the desired. -} appScaleFactor :: Double -> AppConfig e appScaleFactor factor = def {   _apcScaleFactor = Just factor+}++{-|+Whether display scaling detection should not be attempted. If set to True, the+display scale will be set to 1. This flag does not cause an effect on macOS.++Disabling auto scaling also affects window size on Linux and Windows in the+cases where the library would have applied scaling. This happens because window+and viewport size are the same in those operating systems. Window size can be+adjusted with 'appWindowState'.++The logic for detecting display scaling varies depending on the platform:++__macOS__++Scaling can be detected based on the window size and viewport size; the ratio+between these two give the scaling factor.++Using window and viewport size for detecting DPI only works on macOS; both+Windows and Linux return the same value for window and viewport size.++__Windows__++SDL_GetDisplayDPI returns the DPI of the screen, and dividing by 96 gives the+scaling factor. This factor is used to scale the window size and the content.++__Linux__++The situation is more complex, since SDL_GetDisplayDPI does not always return+valid information. There is not a practical DPI/scale detection solution that+works for all combinations of Linux display servers and window managers. Even+when using the most popular window managers, the scaling factor may be handled+differently by the distribution (GNOME in Ubuntu). For a reference of some of+the existing options for DPI scaling detection, check here:+https://wiki.archlinux.org/title/HiDPI.++Considering the above, when SDL_GetDisplayDPI fails, the library assumes that a+screen width larger than 1920 belongs to an HiDPI display and uses a scale+factor of 2. This factor is used to scale the window size and the content.+-}+appDisableAutoScale :: Bool -> AppConfig e+appDisableAutoScale disable = def {+  _apcDisableAutoScale = Just disable }  {-|
src/Monomer/Main/Util.hs view
@@ -21,15 +21,14 @@ import Control.Monad.State import Data.Default import Data.Maybe-import Safe (headMay)  import qualified Data.Sequence as Seq import qualified Data.Map as Map-import qualified Graphics.Rendering.OpenGL as GL import qualified SDL  import Monomer.Core import Monomer.Event+import Monomer.Helper (headMay) import Monomer.Main.Platform import Monomer.Main.Types import Monomer.Widgets.Util.Widget@@ -52,7 +51,7 @@   _mcWindowSize = winSize,   _mcDpr = dpr,   _mcEpr = epr,-  _mcRenderChannel = channel,+  _mcRenderMethod = Right channel,   _mcInputStatus = def,   _mcCursorStack = [],   _mcFocusedWidgetId = def,
src/Monomer/Widgets.hs view
@@ -9,12 +9,13 @@ Widgets module, grouping and re-exporting all the existing widgets. -} module Monomer.Widgets (+  -- * Composite widget   module Monomer.Widgets.Composite,-+  -- * Animation   module Monomer.Widgets.Animation.Fade,   module Monomer.Widgets.Animation.Slide,   module Monomer.Widgets.Animation.Types,-+  -- * Containers   module Monomer.Widgets.Containers.Alert,   module Monomer.Widgets.Containers.Box,   module Monomer.Widgets.Containers.Confirm,@@ -30,7 +31,7 @@   module Monomer.Widgets.Containers.ThemeSwitch,   module Monomer.Widgets.Containers.Tooltip,   module Monomer.Widgets.Containers.ZStack,-+  -- * Single widgets   module Monomer.Widgets.Singles.Button,   module Monomer.Widgets.Singles.Checkbox,   module Monomer.Widgets.Singles.ColorPicker,
src/Monomer/Widgets/Animation/Fade.hs view
@@ -54,7 +54,7 @@ -} data FadeCfg e = FadeCfg {   _fdcAutoStart :: Maybe Bool,-  _fdcDuration :: Maybe Int,+  _fdcDuration :: Maybe Millisecond,   _fdcOnFinished :: [e] } deriving (Eq, Show) @@ -80,7 +80,7 @@     _fdcAutoStart = Just start   } -instance CmbDuration (FadeCfg e) Int where+instance CmbDuration (FadeCfg e) Millisecond where   duration dur = def {     _fdcDuration = Just dur   }@@ -92,7 +92,7 @@  data FadeState = FadeState {   _fdsRunning :: Bool,-  _fdsStartTs :: Int+  _fdsStartTs :: Millisecond } deriving (Eq, Show, Generic)  instance Default FadeState where@@ -141,7 +141,7 @@   autoStart = fromMaybe False (_fdcAutoStart config)   duration = fromMaybe 500 (_fdcDuration config)   period = 20-  steps = duration `div` period+  steps = fromIntegral $ duration `div` period    finishedReq node = delayedMessage node AnimationFinished duration   renderReq wenv node = req where
src/Monomer/Widgets/Animation/Slide.hs view
@@ -66,7 +66,7 @@ data SlideCfg e = SlideCfg {   _slcDirection :: Maybe SlideDirection,   _slcAutoStart :: Maybe Bool,-  _slcDuration :: Maybe Int,+  _slcDuration :: Maybe Millisecond,   _slcOnFinished :: [e] } deriving (Eq, Show) @@ -94,7 +94,7 @@     _slcAutoStart = Just start   } -instance CmbDuration (SlideCfg e) Int where+instance CmbDuration (SlideCfg e) Millisecond where   duration dur = def {     _slcDuration = Just dur   }@@ -122,7 +122,7 @@  data SlideState = SlideState {   _slsRunning :: Bool,-  _slsStartTs :: Int+  _slsStartTs :: Millisecond } deriving (Eq, Show, Generic)  instance Default SlideState where@@ -174,7 +174,7 @@   autoStart = fromMaybe False (_slcAutoStart config)   duration = fromMaybe 500 (_slcDuration config)   period = 20-  steps = duration `div` period+  steps = fromIntegral $ duration `div` period    finishedReq node = delayedMessage node AnimationFinished duration   renderReq wenv node = req where
src/Monomer/Widgets/Composite.hs view
@@ -11,13 +11,15 @@ the need to implement a lower level widget. It can comunicate with its parent component by reporting events. -Requires two main functions:+Requires two functions:  - UI Builder: creates the widget tree based on the provided Widget Environment and model. This widget tree is made of other widgets, in general combinations of containers and singles. - Event Handler: processes user defined events which are raised by the widgets created when building the UI.++Composite is discussed in detail in the tutorials. -} {-# LANGUAGE BangPatterns #-} {-# LANGUAGE ConstraintKinds #-}@@ -42,14 +44,16 @@   CompositeEvent,   MergeRequired,   MergeReqsHandler,-  CompositeCustomModelBuilder,+  MergeEventsHandler,+  MergeModelHandler,   EventHandler,   UIBuilder,   TaskHandler,   ProducerHandler,   CompMsgUpdate,   compositeMergeReqs,-  customModelBuilder,+  compositeMergeEvents,+  compositeMergeModel,    -- * Constructors   composite,@@ -93,26 +97,40 @@ type CompositeEvent e = WidgetEvent e  -- | Checks if merging the composite is required.-type MergeRequired s-  = s     -- ^ Old composite model.-  -> s    -- ^ New composite model-  -> Bool -- ^ True if merge is required.+type MergeRequired s e+  = WidgetEnv s e  -- ^ Widget environment.+  -> s             -- ^ Old composite model.+  -> s             -- ^ New composite model+  -> Bool          -- ^ True if merge is required.  -- | Generates requests during the merge process.-type MergeReqsHandler s e+type MergeReqsHandler s e sp   = WidgetEnv s e         -- ^ Widget environment.   -> WidgetNode s e       -- ^ New widget node.   -> WidgetNode s e       -- ^ Old widget node.-  -> s                    -- ^ The current model.+  -> sp                   -- ^ Parent model.+  -> s                    -- ^ Old composite model.+  -> s                    -- ^ New composite model.   -> [WidgetRequest s e]  -- ^ The list of requests. --- | Creates a custom composite model from the parent model.-type CompositeCustomModelBuilder s sp-  = sp -- ^ Parent model.-  -> s -- ^ Old custom composite model.-  -> s -- ^ New custom composite model.-  -> s -- ^ Custom composite model.+-- | Generates events during the merge process.+type MergeEventsHandler s e sp+  = WidgetEnv s e         -- ^ Widget environment.+  -> WidgetNode s e       -- ^ New widget node.+  -> WidgetNode s e       -- ^ Old widget node.+  -> sp                   -- ^ Parent model.+  -> s                    -- ^ Old composite model.+  -> s                    -- ^ New composite model.+  -> [e]                  -- ^ The list of events. +-- | Allows updating the composite model with information from the parent model.+type MergeModelHandler s e sp+  = WidgetEnv s e         -- ^ Widget environment.+  -> sp                   -- ^ Parent model.+  -> s                    -- ^ Old composite model.+  -> s                    -- ^ New composite model.+  -> s                    -- ^ Updated composite model.+ -- | Handles a composite event and returns a set of responses. type EventHandler s e sp ep   = WidgetEnv s e@@ -193,51 +211,53 @@ {-| Configuration options for composite: +- 'onInit': event to raise when the widget is created. Useful for initializing+  required resources.+- 'onDispose': event to raise when the widget is disposed. Useful for freeing+  acquired resources.+- 'onResize': event to raise when the size of the widget changes.+- 'onChange': event to raise when the model changes. The value passed to the+  provided event is the previous version of the model. The current version of+  the model is always available as a parameter in the _handleEvent_ function.+- 'onChangeReq': 'WidgetRequest' to generate when the model changes.+- 'onEnabledChange': event to raise when the enabled status changes.+- 'onVisibleChange': event to raise when the visibility changes. - 'mergeRequired': indicates if merging is necessary for this widget. In case   the UI build process references information outside the model, it can be used   to signal that merging is required even if the model has not changed. It can   also be used as a performance tweak if the changes do not require rebuilding   the UI.-- 'onInit': event to raise when the widget is created. Useful for performing all-  kinds of initialization.-- 'onDispose': event to raise when the widget is disposed. Used to free-  resources.-- 'onResize': event to raise when the size of the widget changes.-- 'onChange': event to raise when the size of the model changes.-- 'onChangeReq': 'WidgetRequest' to generate when the size of the widget-  changes.-- 'onEnabledChange': event to raise when the enabled status changes.-- 'onVisibleChange': event to raise when the visibility changes. - 'compositeMergeReqs': functions to generate WidgetRequests during the merge   process. Since merge is already handled by Composite (by merging its tree),   this is complementary for the cases when more control, and the previous   version of the widget tree, is required.  For example, it is used in   'Monomer.Widgets.Containers.Confirm' to set the focus on its Accept button-  when visibility is restored (usually means it was brought to the front in a-  zstack, and the visibility flag of the previous version needs to be checked).-- 'customModelBuilder': function for extracting a custom model from the current-  parent model and the previous composite model. Useful when the composite needs-  a more complex model than what the user is binding.+  when visibility is restored (this usually means it was brought to the front in+  a zstack, and the visibility flag of the previous version needs to be+  checked).+- 'compositeMergeModel': Allows updating the composite model with information+  from the parent model. Useful when the composite needs a more complex model+  than what the user is binding. -} data CompositeCfg s e sp ep = CompositeCfg {-  _cmcModelBuilder :: Maybe (CompositeCustomModelBuilder s sp),-  _cmcMergeRequired :: Maybe (MergeRequired s),-  _cmcMergeReqs :: [MergeReqsHandler s e],-  _cmcOnInit :: [e],-  _cmcOnDispose :: [e],+  _cmcMergeRequired :: Maybe (MergeRequired s e),+  _cmcMergeReqs :: [MergeReqsHandler s e sp],+  _cmcMergeModel :: Maybe (MergeModelHandler s e sp),+  _cmcOnInitReq :: [WidgetRequest s e],+  _cmcOnDisposeReq :: [WidgetRequest s e],   _cmcOnResize :: [Rect -> e],-  _cmcOnChangeReq :: [s -> WidgetRequest sp ep],+  _cmcOnChangeReq :: [s -> WidgetRequest s e],   _cmcOnEnabledChange :: [e],   _cmcOnVisibleChange :: [e] }  instance Default (CompositeCfg s e sp ep) where   def = CompositeCfg {-    _cmcModelBuilder = Nothing,+    _cmcMergeModel = Nothing,     _cmcMergeRequired = Nothing,     _cmcMergeReqs = [],-    _cmcOnInit = [],-    _cmcOnDispose = [],+    _cmcOnInitReq = [],+    _cmcOnDisposeReq = [],     _cmcOnResize = [],     _cmcOnChangeReq = [],     _cmcOnEnabledChange = [],@@ -246,11 +266,11 @@  instance Semigroup (CompositeCfg s e sp ep) where   (<>) c1 c2 = CompositeCfg {-    _cmcModelBuilder = _cmcModelBuilder c2 <|> _cmcModelBuilder c1,+    _cmcMergeModel = _cmcMergeModel c2 <|> _cmcMergeModel c1,     _cmcMergeRequired = _cmcMergeRequired c2 <|> _cmcMergeRequired c1,     _cmcMergeReqs = _cmcMergeReqs c1 <> _cmcMergeReqs c2,-    _cmcOnInit = _cmcOnInit c1 <> _cmcOnInit c2,-    _cmcOnDispose = _cmcOnDispose c1 <> _cmcOnDispose c2,+    _cmcOnInitReq = _cmcOnInitReq c1 <> _cmcOnInitReq c2,+    _cmcOnDisposeReq = _cmcOnDisposeReq c1 <> _cmcOnDisposeReq c2,     _cmcOnResize = _cmcOnResize c1 <> _cmcOnResize c2,     _cmcOnChangeReq = _cmcOnChangeReq c1 <> _cmcOnChangeReq c2,     _cmcOnEnabledChange = _cmcOnEnabledChange c1 <> _cmcOnEnabledChange c2,@@ -260,32 +280,42 @@ instance Monoid (CompositeCfg s e sp ep) where   mempty = def -instance CmbMergeRequired (CompositeCfg s e sp ep) s where+instance CmbMergeRequired (CompositeCfg s e sp ep) (WidgetEnv s e) s where   mergeRequired fn = def {     _cmcMergeRequired = Just fn   } -instance CmbOnInit (CompositeCfg s e sp ep) e where+instance WidgetEvent e => CmbOnInit (CompositeCfg s e sp ep) e where   onInit fn = def {-    _cmcOnInit = [fn]+    _cmcOnInitReq = [RaiseEvent fn]   } -instance CmbOnDispose (CompositeCfg s e sp ep) e where+instance CmbOnInitReq (CompositeCfg s e sp ep) s e where+  onInitReq req = def {+    _cmcOnInitReq = [req]+  }++instance WidgetEvent e => CmbOnDispose (CompositeCfg s e sp ep) e where   onDispose fn = def {-    _cmcOnDispose = [fn]+    _cmcOnDisposeReq = [RaiseEvent fn]   } +instance CmbOnDisposeReq (CompositeCfg s e sp ep) s e where+  onDisposeReq req = def {+    _cmcOnDisposeReq = [req]+  }+ instance CmbOnResize (CompositeCfg s e sp ep) e Rect where   onResize fn = def {     _cmcOnResize = [fn]   } -instance WidgetEvent ep => CmbOnChange (CompositeCfg s e sp ep) s ep where+instance WidgetEvent e => CmbOnChange (CompositeCfg s e sp ep) s e where   onChange fn = def {     _cmcOnChangeReq = [RaiseEvent . fn]   } -instance CmbOnChangeReq (CompositeCfg s e sp ep) sp ep s where+instance CmbOnChangeReq (CompositeCfg s e sp ep) s e s where   onChangeReq req = def {     _cmcOnChangeReq = [req]   }@@ -300,35 +330,58 @@     _cmcOnVisibleChange = [fn]   } --- | Generate WidgetRequests during the merge process.-compositeMergeReqs :: MergeReqsHandler s e -> CompositeCfg s e sp ep+{-|+Generate WidgetRequests during the merge process.++This function is not called during initialization; 'onInitReq' can be used.+-}+compositeMergeReqs :: MergeReqsHandler s e sp -> CompositeCfg s e sp ep compositeMergeReqs fn = def {   _cmcMergeReqs = [fn] }  {-|-Generates a custom model from the current parent model and the previous-composite model. Useful when the composite needs a more complex model than what-the user is binding.+Generate events during the merge process.++This function is not called during initialization; 'onInit' can be used. -}-customModelBuilder-  :: CompositeCustomModelBuilder s sp-  -> CompositeCfg s e sp ep-customModelBuilder fn = def {-  _cmcModelBuilder = Just fn+compositeMergeEvents+  :: WidgetEvent e => MergeEventsHandler s e sp -> CompositeCfg s e sp ep+compositeMergeEvents fn = cfg where+  cfg = def {+    _cmcMergeReqs = [wrapper]+  }+  wrapper wenv node oldNode parentModel oldModel newModel+    = RaiseEvent <$> fn wenv node oldNode parentModel oldModel newModel++{-|+Allows updating the composite model with information from the parent model.+Useful when the composite needs a more complex model than what the user is+binding.++For example, a database record may be binded as the model from the parent, but+the composite needs its own boolean flags to toggle visibility on different+sections.++This function is called during both merge and init. On init, the oldModel will+be equal to the current model.+-}+compositeMergeModel :: MergeModelHandler s e sp -> CompositeCfg s e sp ep+compositeMergeModel fn = def {+  _cmcMergeModel = Just fn }  data Composite s e sp ep = Composite {   _cmpWidgetData :: !(WidgetData sp s),   _cmpEventHandler :: !(EventHandler s e sp ep),   _cmpUiBuilder :: !(UIBuilder s e),-  _cmpMergeRequired :: MergeRequired s,-  _cmpMergeReqs :: [MergeReqsHandler s e],-  _cmpModelBuilder :: Maybe (CompositeCustomModelBuilder s sp),-  _cmpOnInit :: [e],-  _cmpOnDispose :: [e],+  _cmpMergeRequired :: MergeRequired s e,+  _cmpMergeReqs :: [MergeReqsHandler s e sp],+  _cmpMergeModel :: Maybe (MergeModelHandler s e sp),+  _cmpOnInitReq :: [WidgetRequest s e],+  _cmpOnDisposeReq :: [WidgetRequest s e],   _cmpOnResize :: [Rect -> e],-  _cmpOnChangeReq :: [s -> WidgetRequest sp ep],+  _cmpOnChangeReq :: [s -> WidgetRequest s e],   _cmpOnEnabledChange :: [e],   _cmpOnVisibleChange :: [e] }@@ -383,7 +436,7 @@   :: (CompositeModel s, CompositeEvent e, CompositeEvent ep, CompParentModel sp)   => WidgetType              -- ^ The name of the composite.   -> s                       -- ^ The model.-  -> (s -> ep)               -- ^ The event to report when model changes.+  -> (s -> e)                -- ^ The event to report when model changes.   -> UIBuilder s e           -- ^ The UI builder function.   -> EventHandler s e sp ep  -- ^ The event handler.   -> WidgetNode sp ep        -- ^ The resulting widget.@@ -398,7 +451,7 @@   :: (CompositeModel s, CompositeEvent e, CompositeEvent ep, CompParentModel sp)   => WidgetType                -- ^ The name of the composite.   -> s                         -- ^ The model.-  -> (s -> ep)                 -- ^ The event to report when model changes.+  -> (s -> e)                  -- ^ The event to report when model changes.   -> UIBuilder s e             -- ^ The UI builder function.   -> EventHandler s e sp ep    -- ^ The event handler.   -> [CompositeCfg s e sp ep]  -- ^ The config options.@@ -419,7 +472,7 @@   -> WidgetNode sp ep          -- ^ The resulting widget. compositeD_ wType wData uiBuilder evtHandler configs = newNode where   config = mconcat configs-  mergeReq = fromMaybe (/=) (_cmcMergeRequired config)+  mergeReq = fromMaybe (const (/=)) (_cmcMergeRequired config)   !widgetRoot = spacer   composite = Composite {     _cmpWidgetData = wData,@@ -427,9 +480,9 @@     _cmpUiBuilder = uiBuilder,     _cmpMergeRequired = mergeReq,     _cmpMergeReqs = _cmcMergeReqs config,-    _cmpModelBuilder = _cmcModelBuilder config,-    _cmpOnInit = _cmcOnInit config,-    _cmpOnDispose = _cmcOnDispose config,+    _cmpMergeModel = _cmcMergeModel config,+    _cmpOnInitReq = _cmcOnInitReq config,+    _cmpOnDisposeReq = _cmcOnDisposeReq config,     _cmpOnResize = _cmcOnResize config,     _cmpOnChangeReq = _cmcOnChangeReq config,     _cmpOnEnabledChange = _cmcOnEnabledChange config,@@ -472,11 +525,12 @@ compositeInit comp state wenv widgetComp = newResult where   CompositeState{..} = state -  !customModelBuilder = _cmpModelBuilder comp+  !mergeModel = _cmpMergeModel comp   !parentModel = wenv ^. L.model   !userModel = getUserModel comp wenv-  !model = case customModelBuilder of-    Just buildCustomModel -> buildCustomModel parentModel userModel userModel+  !model = case mergeModel of+    Just merge -> merge cwenv parentModel userModel userModel where+      !cwenv = convertWidgetEnv wenv _cpsWidgetKeyMap userModel     _ -> userModel    -- Creates UI using provided function@@ -491,11 +545,11 @@     _cpsWidgetKeyMap = collectWidgetKeys M.empty root   } -  !newEvts = RaiseEvent <$> Seq.fromList (_cmpOnInit comp)-   getBaseStyle wenv node = Nothing   styledComp = initNodeStyle getBaseStyle wenv widgetComp-  tempResult = WidgetResult root (RenderOnce <| reqs <> newEvts)++  initReqs = Seq.fromList (_cmpOnInitReq comp)+  tempResult = WidgetResult root (RenderOnce <| reqs <> initReqs)   !newResult = toParentResult comp newState wenv styledComp tempResult  -- | Merge@@ -513,11 +567,12 @@   validState = fromMaybe state (useState oldState)   CompositeState oldModel oldRoot oldWidgetKeys = validState -  !customModelBuilder = _cmpModelBuilder comp+  !mergeModel = _cmpMergeModel comp   !parentModel = wenv ^. L.model   !userModel = getUserModel comp wenv-  !model = case customModelBuilder of-    Just buildCustomModel -> buildCustomModel parentModel (fromJust oldModel) userModel+  !model = case mergeModel of+    Just merge -> merge cwenv parentModel (fromJust oldModel) userModel where+      cwenv = convertWidgetEnv wenv oldWidgetKeys userModel     _ -> userModel    -- Creates new UI using provided function@@ -527,7 +582,7 @@   -- Needed in case the user references something outside model when building UI   -- The same model is provided as old since nothing else is available, but   -- mergeRequired may be using data from a closure-  modelChanged = _cmpMergeRequired comp (fromJust oldModel) model+  modelChanged = _cmpMergeRequired comp cwenv (fromJust oldModel) model   visibleChg = nodeVisibleChanged oldComp newComp   enabledChg = nodeEnabledChanged oldComp newComp   flagsChanged = visibleChg || enabledChg@@ -563,7 +618,8 @@   evts = RaiseEvent <$> Seq.fromList (visibleEvts ++ enabledEvts)    mergeReqsFns = _cmpMergeReqs comp-  mergeReqs = concatMap (\fn -> fn cwenv newRoot oldRoot model) mergeReqsFns+  mergeHelper f = f cwenv newRoot oldRoot parentModel (fromJust oldModel) model+  mergeReqs = concatMap mergeHelper mergeReqsFns   extraReqs = seqCatMaybes (toParentReq widgetId <$> Seq.fromList mergeReqs)    tmpResult = WidgetResult newRoot (RenderOnce <| tmpReqs <> extraReqs <> evts)@@ -586,10 +642,11 @@   model = getCompositeModel state   cwenv = convertWidgetEnv wenv _cpsWidgetKeyMap model   widget = _cpsRoot ^. L.widget-  newEvts = RaiseEvent <$> Seq.fromList (_cmpOnDispose comp)    WidgetResult _ reqs = widgetDispose widget cwenv _cpsRoot-  tempResult = WidgetResult _cpsRoot (reqs <> newEvts)++  disposeReqs = Seq.fromList (_cmpOnDisposeReq comp)+  tempResult = WidgetResult _cpsRoot (reqs <> disposeReqs)   result = toParentResult comp state wenv widgetComp tempResult  compositeGetInstanceTree@@ -911,8 +968,9 @@   -> WidgetNode s e   -> WidgetNode sp ep   -> WidgetResult sp ep-mergeChild comp state wenv newModel widgetRoot widgetComp = newResult where+mergeChild comp state wenv newModel widgetRoot widgetComp = parentResult where   CompositeState{..} = state+  oldModel = getCompositeModel state   cwenv = convertWidgetEnv wenv _cpsWidgetKeyMap newModel   widgetId = _cpsRoot ^. L.info . L.widgetId   builtRoot = cascadeCtx wenv widgetComp (_cmpUiBuilder comp cwenv newModel)@@ -927,12 +985,14 @@     _cpsRoot = mergedResult ^. L.node,     _cpsWidgetKeyMap = collectWidgetKeys M.empty (mergedResult ^. L.node)   }-  !result = toParentResult comp mergedState wenv widgetComp mergedResult-  !newReqs = widgetDataSet (_cmpWidgetData comp) newModel-    ++ fmap ($ newModel) (_cmpOnChangeReq comp)+  childReqs = fmap ($ oldModel) (_cmpOnChangeReq comp)+  parentReqs = widgetDataSet (_cmpWidgetData comp) newModel     ++ [ResizeWidgets widgetId | initRequired]-  !newResult = result-    & L.requests <>~ Seq.fromList newReqs+  childResult = mergedResult+    & L.requests <>~ Seq.fromList childReqs+  result = toParentResult comp mergedState wenv widgetComp childResult+  parentResult = result+    & L.requests .~ Seq.fromList parentReqs <> result ^. L.requests  getUserModel   :: (CompositeModel s, CompositeEvent e, CompositeEvent ep, CompParentModel sp)@@ -1001,6 +1061,7 @@ convertWidgetEnv wenv widgetKeyMap model = WidgetEnv {   _weOs = _weOs wenv,   _weDpr = _weDpr wenv,+  _weAppStartTs = _weAppStartTs wenv,   _weFontManager = _weFontManager wenv,   _weFindBranchByPath = _weFindBranchByPath wenv,   _weMainButton = _weMainButton wenv,
src/Monomer/Widgets/Container.hs view
@@ -25,6 +25,7 @@   -- * Configuration   ContainerGetBaseStyle,   ContainerGetCurrentStyle,+  ContainerCreateContainerFromModel,   ContainerUpdateCWenvHandler,   ContainerInitHandler,   ContainerInitPostHandler,@@ -680,13 +681,13 @@   -> WidgetNode s e   -> WidgetResult s e   -> WidgetResult s e-mergeChildren updateCWenv !wenv !newNode !oldNode !result = newResult where-  WidgetResult uNode uReqs = result+mergeChildren updateCWenv !wenv !newNode !oldNode !pResult = newResult where+  WidgetResult pNode pReqs = pResult   oldChildren = oldNode ^. L.children   oldIts = Seq.mapWithIndex (,) oldChildren-  updatedChildren = uNode ^. L.children+  updatedChildren = pNode ^. L.children -  mergeChild idx child = (idx, cascadeCtx wenv uNode child idx)+  mergeChild idx child = (idx, cascadeCtx wenv pNode child idx)   newIts = Seq.mapWithIndex mergeChild updatedChildren   oldKeys = buildLocalMap oldChildren   newKeys = buildLocalMap (snd <$> newIts)@@ -696,8 +697,8 @@   mergedChildren = fmap _wrNode mergedResults   mergedReqs = foldMap _wrRequests mergedResults   removedReqs = foldMap _wrRequests removedResults-  mergedNode = uNode & L.children .~ mergedChildren-  newReqs = uReqs <> mergedReqs <> removedReqs+  mergedNode = pNode & L.children .~ mergedChildren+  newReqs = pReqs <> mergedReqs <> removedReqs   !newResult = WidgetResult mergedNode newReqs  mergeChildSeq
src/Monomer/Widgets/Containers/Alert.hs view
@@ -8,6 +8,24 @@  Simple alert dialog, displaying a close button and optional title. Usually embedded in a zstack component and displayed/hidden depending on context.++A simple text message can be displayed with 'alertMsg', providing the message+text and the event to generate when the user closes the alert:++@+alertMsg "En error occurred" AlertClosedEvent+@++Alternatively, a custom widget can be provided to display as content:++@+customAlert = alert AlertClosedEvent content where+  content = hstack [+      label "Error:",+      filler,+      label errorDescription+    ]+@ -} {-# LANGUAGE Strict #-} 
src/Monomer/Widgets/Containers/Base/LabeledItem.hs view
@@ -8,6 +8,11 @@  Container for items with an associated clickable label. Mainly used with radio and checkbox.++For usage examples, see:++- "Monomer.Widgets.Singles.LabeledCheckbox"+- "Monomer.Widgets.Singles.LabeledRadio" -} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE RankNTypes #-}
src/Monomer/Widgets/Containers/Box.hs view
@@ -6,19 +6,45 @@ Stability   : experimental Portability : non-portable -Container for a single item.+Container for a single item, providing functionalities that may not be available+in other widgets.  Useful in different layout situations, since it provides alignment options. This allows for the inner widget to keep its size while being positioned more explicitly, while the box takes up the complete space assigned by its parent (in-particular for containers which do not follow SizeReq restriccions, such as+particular for containers which do not follow SizeReq restrictions, such as Grid). +@+box_ [alignRight, alignBottom] $+  image "assets/test-image.jpg"+    \`styleBasic\` [width 100, height 100]+@+ Can be used to add padding to an inner widget with a border. This is equivalent to the margin property in CSS. +@+-- Padding is inside the border+content = label \"Message\"+  \`styleBasic\` [padding 5, border 1 black]+-- Padding in the wrapper box acts as margin+container = box content+  \`styleBasic\` [padding 5]+@+ Also useful to handle click events in complex widget structures (for example, a label with an image at its side).++@+content = vstack [+    label "All the content widget is clickable",+    spacer,+    image "assets/test-image.jpg"+  ]+clickableItem = box_ [onClick ItemClicked] content+  \`styleBasic\' [cursorHand]+@ -} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-}@@ -80,7 +106,7 @@   _boxExpandContent :: Maybe Bool,   _boxIgnoreEmptyArea :: Maybe Bool,   _boxSizeReqUpdater :: [SizeReqUpdater],-  _boxMergeRequired :: Maybe (s -> s -> Bool),+  _boxMergeRequired :: Maybe (WidgetEnv s e -> s -> s -> Bool),   _boxAlignH :: Maybe AlignH,   _boxAlignV :: Maybe AlignV,   _boxOnFocusReq :: [Path -> WidgetRequest s e],@@ -142,7 +168,7 @@     _boxSizeReqUpdater = [updater]   } -instance CmbMergeRequired (BoxCfg s e) s where+instance CmbMergeRequired (BoxCfg s e) (WidgetEnv s e) s where   mergeRequired fn = def {     _boxMergeRequired = Just fn   }@@ -320,7 +346,7 @@   mergeRequired wenv node oldNode oldState = required where     newModel = wenv ^. L.model     required = case (_boxMergeRequired config, _bxsModel oldState) of-      (Just mergeReqFn, Just oldModel) -> mergeReqFn oldModel newModel+      (Just mergeReqFn, Just oldModel) -> mergeReqFn wenv oldModel newModel       _ -> True    merge wenv node oldNode oldState = resultNode newNode where
src/Monomer/Widgets/Containers/Confirm.hs view
@@ -6,9 +6,30 @@ Stability   : experimental Portability : non-portable -Simple confirm dialog, displaying an accept and close buttons and optional-title. Usually embedded in a zstack component and displayed/hidden depending on+Simple confirm dialog, displaying accept and close buttons and optional title.+Usually embedded in a zstack component and displayed/hidden depending on context.++Similar to "Monomer.Widgets.Containers.Alert", but takes two events to handle+the Accept and Cancel actions.++A simple text message can be displayed with 'confirmMsg', providing the message+text and the events to generate when the user interacts with the dialog:++@+confirmMsg "Save changes?" ConfirmAcceptEvent ConfirmCancelEvent+@++Alternatively, a custom widget can be provided to display as content:++@+customConfirm = confirm ConfirmAcceptEvent ConfirmCancelEvent where+  content = hstack [+      label "Save changes?",+      filler,+      label fileName+    ]+@ -} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE RankNTypes #-}@@ -17,6 +38,7 @@ module Monomer.Widgets.Containers.Confirm (   -- * Configuration   ConfirmCfg,+  InnerConfirmEvt,   -- * Constructors   confirm,   confirm_,@@ -89,28 +111,34 @@     _cfcCancel = Just t   } -newtype ConfirmEvt e+{-|+Wraps the user event to be able to implement custom events and still be able to+report to its parent.++Previously used, now kept for reference.+-}+newtype InnerConfirmEvt e   = ConfirmParentEvt e   deriving (Eq, Show)  -- | Creates a confirm dialog with the provided content. confirm   :: (WidgetModel s, WidgetEvent e)-  => e                             -- ^ The accept button event.-  -> e                             -- ^ The cancel button event.-  -> WidgetNode () (ConfirmEvt e)  -- ^ The content to display in the dialog.-  -> WidgetNode s e               -- ^ The created dialog.+  => e                                  -- ^ The accept button event.+  -> e                                  -- ^ The cancel button event.+  -> WidgetNode () (InnerConfirmEvt e)  -- ^ Content to display in the dialog.+  -> WidgetNode s e                     -- ^ The created dialog. confirm acceptEvt cancelEvt dialogBody = newNode where   newNode = confirm_ acceptEvt cancelEvt def dialogBody  -- | Creates an alert dialog with the provided content. Accepts config. confirm_   :: (WidgetModel s, WidgetEvent e)-  => e                             -- ^ The accept button event.-  -> e                             -- ^ The cancel button event.-  -> [ConfirmCfg]                   -- ^ The config options for the dialog.-  -> WidgetNode () (ConfirmEvt e)  -- ^ The content to display in the dialog.-  -> WidgetNode s e               -- ^ The created dialog.+  => e                                  -- ^ The accept button event.+  -> e                                  -- ^ The cancel button event.+  -> [ConfirmCfg]                       -- ^ The config options for the dialog.+  -> WidgetNode () (InnerConfirmEvt e)  -- ^ Content to display in the dialog.+  -> WidgetNode s e                     -- ^ The created dialog. confirm_ acceptEvt cancelEvt configs dialogBody = newNode where   config = mconcat configs   createUI = buildUI (const dialogBody) acceptEvt cancelEvt config@@ -120,19 +148,19 @@ -- | Creates an alert dialog with a text message as content. confirmMsg   :: (WidgetModel s, WidgetEvent e)-  => Text              -- ^ The message to display in the dialog.-  -> e                -- ^ The accept button event.-  -> e                -- ^ The cancel button event.+  => Text            -- ^ The message to display in the dialog.+  -> e               -- ^ The accept button event.+  -> e               -- ^ The cancel button event.   -> WidgetNode s e  -- ^ The created dialog. confirmMsg msg acceptEvt cancelEvt = confirmMsg_ msg acceptEvt cancelEvt def  -- | Creates an alert dialog with a text message as content. Accepts config. confirmMsg_   :: (WidgetModel s, WidgetEvent e)-  => Text              -- ^ The message to display in the dialog.-  -> e                -- ^ The accept button event.-  -> e                -- ^ The cancel button event.-  -> [ConfirmCfg]      -- ^ The config options for the dialog.+  => Text            -- ^ The message to display in the dialog.+  -> e               -- ^ The accept button event.+  -> e               -- ^ The cancel button event.+  -> [ConfirmCfg]    -- ^ The config options for the dialog.   -> WidgetNode s e  -- ^ The created dialog. confirmMsg_ message acceptEvt cancelEvt configs = newNode where   config = mconcat configs@@ -142,8 +170,8 @@   compCfg = [compositeMergeReqs mergeReqs]   newNode = compositeD_ "confirm" (WidgetValue ()) createUI handleEvent compCfg -mergeReqs :: MergeReqsHandler s e-mergeReqs wenv newNode oldNode model = reqs where+mergeReqs :: MergeReqsHandler s e sp+mergeReqs wenv newNode oldNode parentModel oldModel model = reqs where   acceptPath = SetFocus <$> widgetIdFromKey wenv "acceptBtn"   isVisible node = node ^. L.info . L.visible   reqs@@ -152,13 +180,13 @@  buildUI   :: (WidgetModel s, WidgetEvent ep)-  => (WidgetEnv s (ConfirmEvt ep) -> WidgetNode s (ConfirmEvt ep))+  => (WidgetEnv s (InnerConfirmEvt ep) -> WidgetNode s (InnerConfirmEvt ep))   -> ep   -> ep   -> ConfirmCfg-  -> WidgetEnv s (ConfirmEvt ep)+  -> WidgetEnv s (InnerConfirmEvt ep)   -> s-  -> WidgetNode s (ConfirmEvt ep)+  -> WidgetNode s (InnerConfirmEvt ep) buildUI dialogBody pAcceptEvt pCancelEvt config wenv model = mainTree where   acceptEvt = ConfirmParentEvt pAcceptEvt   cancelEvt = ConfirmParentEvt pCancelEvt@@ -190,10 +218,10 @@   mainTree = keystroke [("Esc", cancelEvt)] confirmBox  handleEvent-  :: WidgetEnv s (ConfirmEvt ep)-  -> WidgetNode s (ConfirmEvt ep)+  :: WidgetEnv s (InnerConfirmEvt ep)+  -> WidgetNode s (InnerConfirmEvt ep)   -> s-  -> ConfirmEvt ep-  -> [EventResponse s (ConfirmEvt ep) sp ep]+  -> InnerConfirmEvt ep+  -> [EventResponse s (InnerConfirmEvt ep) sp ep] handleEvent wenv node model evt = case evt of   ConfirmParentEvt pevt -> [Report pevt]
src/Monomer/Widgets/Containers/Draggable.hs view
@@ -8,7 +8,16 @@  Draggable container for a single item. Useful for adding drag support without having to implement a custom widget. Usually works in tandem with-'Monomer.Widgets.Containers.DropTarget'.+"Monomer.Widgets.Containers.DropTarget".++Requires a value to identify the content (used when the item is dropped) and the+content to display.++@+dragItem = draggable "item" $ label "This label is draggable"+@++See Tutorial 6 (Composite) for a usage example. -} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-}
src/Monomer/Widgets/Containers/DropTarget.hs view
@@ -8,10 +8,18 @@  Drop target container for a single element. Useful for adding drag support without having to implement a custom widget. Usually works in tandem with-'Monomer.Widgets.Containers.Draggable'.+"Monomer.Widgets.Containers.Draggable".  Raises a user provided event when an item is dropped. The type must match with-the dragged message, otherwise it will not be raised.+the type of the dragged widget message, otherwise it will not be raised.++@+target = dropTarget ItemDropped $+  vstack itemsRows+    \`styleBasic\` [width 200, height 400]+@++See Tutorial 6 (Composite) for a usage example. -} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-}
src/Monomer/Widgets/Containers/Dropdown.hs view
@@ -7,9 +7,21 @@ Portability : non-portable  Dropdown widget, allowing selection of a single item from a collapsable list.-Both header and list content is customizable, and so is its styling. In case-only 'Text' content is needed, 'Monomer.Widgets.Singles.TextDropdown' is easier-to use.+Both header and list content are customizable, and so is their styling.++In case only 'Text' content is needed, "Monomer.Widgets.Singles.TextDropdown" is+easier to use.++@+makeSelected username = hstack [+    label "Selected: ",+    spacer,+    label username+  ]+makeRow username = label username++customDropdown = dropdown userLens usernames makeSelected makeRow+@ -} {-# LANGUAGE BangPatterns #-} {-# LANGUAGE ConstraintKinds #-}
src/Monomer/Widgets/Containers/Grid.hs view
@@ -6,9 +6,23 @@ Stability   : experimental Portability : non-portable -Layout container which distributes size equally along the main axis. For hgrid-it requests max width * elements as its width, and the max height as its height.-The reverse happens for vgrid.+Layout container which distributes space evenly along the main axis. For the+secondary axis children will receive as much space as available for the grid+widget itself.++In the same way as with hstack and vstack, 'hgrid' and 'vgrid' can be combined+to create more complex layouts.++The hgrid widget requests maxWidth * elements as its width, and the max height+as its height. The inverse happens for vgrid.++@+hgrid [+    label "Third 1",+    label "Third 2",+    label "Third 3"+  ]+@ -} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE Strict #-}
src/Monomer/Widgets/Containers/Keystroke.hs view
@@ -11,28 +11,62 @@ implementing a widget from scratch, keyboard events are directly available.  The shortcut definitions are provided as a list of tuples of 'Text', containing-the key combination and associated event. The widget handles unordered-combinations of multiple keys at the same time, but does not support ordered-sequences (pressing "a", releasing, then "b" and "c"). The available keys are:+the key combination and associated event, separated by "-". The widget handles+unordered combinations of multiple keys at the same time, but does not support+ordered sequences (pressing "a", releasing, then "b" and "c"). The available+keys are:  - Mod keys: A, Alt, C, Ctrl, Cmd, O, Option, S, Shift - Action keys: Caps, Delete, Enter, Esc, Return, Space, Tab - Arrows: Up, Down, Left, Right - Function keys: F1-F12+- Separator: Dash (since '-' is used for defining keystrokes)+- Symbols: brackets, ^, *, &, etc. - Lowercase letters (uppercase keys are reserved for mod and action keys) - Numbers -These can be combined, for example:+The keys can be combined, for example:  - Copy: "Ctrl-c" or "C-c" - App config: "Ctrl-Shift-p" or "C-S-p"++@+keystroke [("Esc", CancelEditing)] $ hstack [+    label "Username:",+    spacer,+    textField userLens+  ]+@++Note 1: Following the pattern explained in 'CmbIgnoreChildrenEvts', this widget+by default allows children widgets (i.e., focused widgets) that may receive the+events to respond to the pressed keys. If you want to avoid this, and only keep+the keystroke widgets's response when a combination matches, add the+'ignoreChildrenEvts' config option. To clarify: the only keypress event that+will be filtered is the one that causes a combination to match (the last one).++Note 2: Except in the specific cases mentioned here (Ctrl, Cmd, etc), the keys+must be single characters.++Note 3: Full words must be input exactly as indicated (Ctrl, Cmd, etc). Alias+only exist for the keys described here (A for Alt, C for Ctrl/Cmd, etc).++Note 4: Symbols that require pressing the Shift key (^, &, etc) are virtual keys+and share the KeyCode with the symbol associated to the same physical key. This+causes issues when detecting their pressed status, and thus it's not possible to+combine these symbols with letters, numbers or other symbols in the same+keystroke. The same happens with characters that require pressing a combination+of keys (e.g. accented characters). It is still possible to combine them with+mod keys, so using "C-^" or "C-[" should work. If you find that binding a+symbol/complex character does not work, try using the names of the physical keys+instead (e.g. "Shift-e" instead of "E"). -} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FunctionalDependencies #-} {-# LANGUAGE RankNTypes #-}+{-# LANGUAGE StrictData #-} {-# LANGUAGE TemplateHaskell #-}-{-# LANGUAGE Strict #-}  module Monomer.Widgets.Containers.Keystroke (   -- * Configuration@@ -42,6 +76,8 @@   keystroke_ ) where +import Debug.Trace (traceShow)+ import Control.Applicative ((<|>)) import Control.Lens ((&), (^.), (^..), (.~), (%~), _1, at, folded) import Control.Lens.TH (abbreviatedFields, makeLensesWith)@@ -96,7 +132,9 @@   _kstKsCmd :: Bool,   _kstKsAlt :: Bool,   _kstKsShift :: Bool,-  _kstKsKeys :: Set KeyCode+  _kstKsKeys :: Set KeyCode,+  _kstKsKeysText :: Set Text,+  _kstKsErrors :: [Text] } deriving (Eq, Show)  instance Default KeyStroke where@@ -107,13 +145,20 @@     _kstKsCmd = False,     _kstKsAlt = False,     _kstKsShift = False,-    _kstKsKeys = Set.empty+    _kstKsKeys = Set.empty,+    _kstKsKeysText = Set.empty,+    _kstKsErrors = []   }  newtype KeyStrokeState e = KeyStrokeState {   _kssLatest :: [(KeyStroke, e)] } deriving (Eq, Show) +data KeyEntry+  = KeyEntryCode KeyCode+  | KeyEntryText Text+  deriving (Eq, Show)+ makeLensesWith abbreviatedFields ''KeyStroke makeLensesWith abbreviatedFields ''KeyStrokeState @@ -156,37 +201,47 @@       & L.widget .~ makeKeystroke bindings config oldState    handleEvent wenv node target evt = case evt of-    KeyAction mod code KeyPressed -> Just result where-      newWenv = wenv & L.inputStatus %~ removeMods-      matches = filter (keyStrokeActive newWenv code . fst) bindings-      newState = KeyStrokeState matches-      newNode = node-        & L.widget .~ makeKeystroke bindings config newState-      evts = snd <$> matches-      reqs-        | ignoreChildren && not (null evts) = [IgnoreChildrenEvents]-        | otherwise = []-      result = resultReqsEvts newNode reqs evts-    TextInput t-      | ignoreChildren && ignorePrevious t -> Just result where-        previousMatch t = t `elem` _kssLatest state ^.. folded . _1 . ksText-        ignorePrevious t = isTextValidCode t && previousMatch t+    KeyAction mod code KeyPressed -> result where+      result = handleKeystroke (KeyEntryCode code)+    TextInput text+      | ignoreChildren && ignorePrevious text -> Just result where         newState = KeyStrokeState []         newNode = node           & L.widget .~ makeKeystroke bindings config newState         result = resultReqs newNode [IgnoreChildrenEvents]+    TextInput text+      | not (previousMatch text) -> result where+        result = handleKeystroke (KeyEntryText text)     _ -> Nothing     where       ignoreChildren = Just True == _kscIgnoreChildren config+      previousMatch t = t `elem` _kssLatest state ^.. folded . _1 . ksText+      ignorePrevious t = isTextValidCode t && previousMatch t -keyStrokeActive :: WidgetEnv s e -> KeyCode -> KeyStroke -> Bool-keyStrokeActive wenv code ks = currValid && allPressed && validMods where+      handleKeystroke entry = Just result where+        newWenv = wenv & L.inputStatus %~ removeMods+        matches = filter (keyStrokeActive newWenv entry . fst) bindings+        newState = KeyStrokeState matches+        newNode = node+          & L.widget .~ makeKeystroke bindings config newState+        evts = snd <$> matches+        reqs+          | ignoreChildren && not (null evts) = [IgnoreChildrenEvents]+          | otherwise = []+        result = resultReqsEvts newNode reqs evts++keyStrokeActive :: WidgetEnv s e -> KeyEntry -> KeyStroke -> Bool+keyStrokeActive wenv entry ks = currValid && allPressed && validMods where   status = wenv ^. L.inputStatus   keyMod = status ^. L.keyMod   pressedKeys = M.filter (== KeyPressed) (status ^. L.keys) -  currValid = code `elem` (ks ^. ksKeys) || code `elem` modKeys-  allPressed = M.keysSet pressedKeys == ks ^. ksKeys+  (currValid, allPressed, ignoreShift) = case entry of+    KeyEntryCode code -> (valid, pressed, False) where+      valid = code `elem` (ks ^. ksKeys) || code `elem` modKeys+      pressed = M.keysSet pressedKeys == ks ^. ksKeys+    KeyEntryText txt -> (valid, True, True) where+      valid = txt `elem` (ks ^. ksKeysText)    ctrlPressed = isCtrlPressed keyMod   cmdPressed = isMacOS wenv && isGUIPressed keyMod@@ -194,17 +249,24 @@   validC = not (ks ^. ksC) || ks ^. ksC == (ctrlPressed || cmdPressed)   validCtrl = ks ^. ksCtrl == ctrlPressed || ctrlPressed && validC   validCmd = ks ^. ksCmd == cmdPressed || cmdPressed && validC-  validShift = ks ^. ksShift == isShiftPressed keyMod+  validShift = ks ^. ksShift == isShiftPressed keyMod || ignoreShift   validAlt = ks ^. ksAlt == isAltPressed keyMod    validMods = (validC && validCtrl && validCmd) && validShift && validAlt  textToStroke :: Text -> KeyStroke-textToStroke text = ks where+textToStroke text = result where   parts = T.split (=='-') text   ks = foldl' partToStroke def parts     & ksText .~ text +  errors = ks ^. ksErrors+  errorMsg = "'" <> text <> "' is not valid. Invalid parts: "++  result+    | not (T.null text) && null errors = ks+    | otherwise = traceShow (errorMsg, errors) ks+ partToStroke :: KeyStroke -> Text -> KeyStroke partToStroke ks "A" = ks & ksAlt .~ True partToStroke ks "Alt" = ks & ksAlt .~ True@@ -216,9 +278,11 @@ partToStroke ks "S" = ks & ksShift .~ True partToStroke ks "Shift" = ks & ksShift .~ True -- Main keys+partToStroke ks "Backspace" = ks & ksKeys %~ Set.insert keyBackspace partToStroke ks "Caps" = ks & ksKeys %~ Set.insert keyCapsLock partToStroke ks "Delete" = ks & ksKeys %~ Set.insert keyDelete partToStroke ks "Enter" = ks & ksKeys %~ Set.insert keyReturn+partToStroke ks "KpEnter" = ks & ksKeys %~ Set.insert keyPadEnter partToStroke ks "Esc" = ks & ksKeys %~ Set.insert keyEscape partToStroke ks "Return" = ks & ksKeys %~ Set.insert keyReturn partToStroke ks "Space" = ks & ksKeys %~ Set.insert keySpace@@ -242,9 +306,13 @@ partToStroke ks "F11" = ks & ksKeys %~ Set.insert keyF11 partToStroke ks "F12" = ks & ksKeys %~ Set.insert keyF12 -- Other keys (numbers, letters, points, etc)+partToStroke ks "Dash" = partToStroke ks "-" partToStroke ks txt-  | isTextValidCode txt = ks & ksKeys %~ Set.insert (KeyCode (ord txtHead))+  | isTextValidCode txt = ks+      & ksKeys %~ Set.insert (KeyCode (ord txtHead))+      & ksKeysText %~ Set.insert txt   | otherwise = ks+      & ksErrors %~ (++ [txt])   where     txtHead = T.index txt 0 
src/Monomer/Widgets/Containers/Scroll.hs view
@@ -11,10 +11,14 @@ of the inner node with the scroll bars. It also supports automatic focus following. -Messages:+Accepts the following messages:  - 'ScrollTo': Causes the scroll to update its handles to ensure rect is visible. - 'ScrollReset': Sets both handle positions to zero.++@+vscroll (vstack longItemsList)+@ -} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleContexts #-}@@ -68,12 +72,19 @@   | ScrollBoth   deriving (Eq, Show) +{-|+Information provided in the 'onChange' event.++The currently visible viewport is affected by the position of the scroll bars,+their size and any other parent widget that restricts the visible viewport+(e.g., another scroll).+-} data ScrollStatus = ScrollStatus {-  scrollDeltaX :: Double,-  scrollDeltaY :: Double,-  scrollRect :: Rect,-  scrollVpSize :: Size,-  scrollChildSize :: Size+  scrollDeltaX :: Double, -- ^ Displacement in the x axis.+  scrollDeltaY :: Double, -- ^ Displacement in the y axis.+  scrollRect :: Rect,     -- ^ The viewport assigned to the scroll widget.+  scrollVpSize :: Size,   -- ^ The currently visible viewport.+  scrollChildSize :: Size -- ^ The total size of the child widget. } deriving (Eq, Show)  instance Default ScrollStatus where@@ -106,6 +117,7 @@ - 'thumbColor': the color of the thumb. - 'thumbHoverColor': the color of the thumb when mouse is on top. - 'thumbWidth': the width of the thumb.+- 'thumbMinSize': the minimum size of the thumb. - 'thumbRadius': the radius of the corners of the thumb. - 'onChange': event to raise when the viewport changes. - 'onChangeReq': 'WidgetRequest' to generate when the viewport changes.@@ -123,6 +135,7 @@   _scStyle :: Maybe (ALens' ThemeState StyleState),   _scBarWidth :: Maybe Double,   _scThumbWidth :: Maybe Double,+  _scThumbMinSize :: Maybe Double,   _scThumbRadius :: Maybe Double,   _scOnChangeReq :: [ScrollStatus -> WidgetRequest s e] }@@ -141,6 +154,7 @@     _scStyle = Nothing,     _scBarWidth = Nothing,     _scThumbWidth = Nothing,+    _scThumbMinSize = Nothing,     _scThumbRadius = Nothing,     _scOnChangeReq = []   }@@ -159,6 +173,7 @@     _scStyle = _scStyle t2 <|> _scStyle t1,     _scBarWidth = _scBarWidth t2 <|> _scBarWidth t1,     _scThumbWidth = _scThumbWidth t2 <|> _scThumbWidth t1,+    _scThumbMinSize = _scThumbMinSize t2 <|> _scThumbMinSize t1,     _scThumbRadius = _scThumbRadius t2 <|> _scThumbRadius t1,     _scOnChangeReq = _scOnChangeReq t2 <|> _scOnChangeReq t1   }@@ -202,6 +217,11 @@     _scThumbWidth = Just w   } +instance CmbThumbMinSize (ScrollCfg s e) where+  thumbMinSize w = def {+    _scThumbMinSize = Just w+  }+ instance CmbThumbRadius (ScrollCfg s e) where   thumbRadius r = def {     _scThumbRadius = Just r@@ -300,14 +320,16 @@   _sstDragging :: Maybe ActiveBar,   _sstDeltaX :: !Double,   _sstDeltaY :: !Double,+  _sstThumbOffsetX :: !Double,+  _sstThumbOffsetY :: !Double,   _sstVpSize :: Size,   _sstChildSize :: Size,   _sstScissor :: Rect } deriving (Eq, Show, Generic)  data ScrollContext = ScrollContext {-  hScrollRatio :: Double,-  vScrollRatio :: Double,+  hThumbRatio :: Double,+  vThumbRatio :: Double,   hScrollRequired :: Bool,   vScrollRequired :: Bool,   hMouseInScroll :: Bool,@@ -325,6 +347,8 @@     _sstDragging = Nothing,     _sstDeltaX = 0,     _sstDeltaY = 0,+    _sstThumbOffsetX = 0,+    _sstThumbOffsetY = 0,     _sstVpSize = def,     _sstChildSize = def,     _sstScissor = def@@ -395,7 +419,7 @@   }   widget = createContainer state container -  ScrollState dragging dx dy _ _ _ = state+  (dragging, dx, dy) = (_sstDragging state, _sstDeltaX state, _sstDeltaY state)   Size childWidth childHeight = _sstChildSize state   Size maxVpW maxVpH = _sstVpSize state   offset = Point dx dy@@ -484,7 +508,10 @@       follow = fromMaybe (theme ^. L.scrollFollowFocus) (_scFollowFocus config)       overlayMatch = focusOverlay == inOverlay (node ^. L.info) +      fwdFocus = Just (resultReqs node [MoveFocus Nothing FocusFwd])+       result+        | target == node ^. L.info . L.path = fwdFocus         | follow && overlayMatch = focusVp >>= scrollTo wenv node         | otherwise = Nothing @@ -503,9 +530,20 @@       mouseInThumb = hMouseInThumb sctx || vMouseInThumb sctx       mouseInScroll = hMouseInScroll sctx || vMouseInScroll sctx +      thumbOffsetX = point ^. L.x - hThumbRect sctx ^. L.x+      thumbOffsetY = point ^. L.y - vThumbRect sctx ^. L.y+       newState-        | startDragH = state { _sstDragging = Just HBar }-        | startDragV = state { _sstDragging = Just VBar }+        | startDragH = state {+            _sstDragging = Just HBar,+            _sstThumbOffsetX = thumbOffsetX,+            _sstThumbOffsetY = 0+          }+        | startDragV = state {+            _sstDragging = Just VBar,+            _sstThumbOffsetX = 0,+            _sstThumbOffsetY = thumbOffsetY+          }         | jumpScrollH = updateScrollThumb state HBar point contentArea sctx         | jumpScrollV = updateScrollThumb state VBar point contentArea sctx         | mainReleased = state { _sstDragging = Nothing }@@ -627,11 +665,10 @@     ScrollContext{..} = sctx     Rect cx cy _ _ = contentArea -    hMid = _rW hThumbRect / 2-    vMid = _rH vThumbRect / 2+    (offsetH, offsetV) = (_sstThumbOffsetX state, _sstThumbOffsetY state) -    hDelta = (cx - px + hMid) / hScrollRatio-    vDelta = (cy - py + vMid) / vScrollRatio+    hDelta = (cx - px + offsetH) / hThumbRatio+    vDelta = (cy - py + offsetV) / vThumbRatio      newDeltaX       | activeBar == HBar = scrollAxisH hDelta@@ -814,16 +851,19 @@   -> ScrollState   -> Point   -> ScrollContext-scrollStatus config wenv node scrollState mousePos = ScrollContext{..} where-  ScrollState _ dx dy _ _ _ = scrollState-  Size childWidth childHeight = _sstChildSize scrollState-  Size vpWidth vpHeight = _sstVpSize scrollState+scrollStatus config wenv node state mousePos = ScrollContext{..} where+  (dragging, dx, dy) = (_sstDragging state, _sstDeltaX state, _sstDeltaY state)++  Size childWidth childHeight = _sstChildSize state+  Size vpWidth vpHeight = _sstVpSize state+   theme = currentTheme wenv node   style = scrollCurrentStyle wenv node   contentArea = getContentArea node style    barW = fromMaybe (theme ^. L.scrollBarWidth) (_scBarWidth config)   thumbW = fromMaybe (theme ^. L.scrollThumbWidth) (_scThumbWidth config)+  minSize = fromMaybe (theme ^. L.scrollThumbMinSize) (_scThumbMinSize config)    caLeft = _rX contentArea   caTop = _rY contentArea@@ -835,15 +875,24 @@    hRatio = caWidth / childWidth   vRatio = caHeight / childHeight-  hRatioR = (caWidth - barW) / childWidth-  vRatioR = (caHeight - barW) / childHeight -  (hScrollRatio, vScrollRatio)-    | hRatio < 1 && vRatio < 1 = (hRatioR, vRatioR)-    | otherwise = (hRatio, vRatio)+  ratioBarW+    | hRatio < 1 && vRatio < 1 = barW+    | otherwise = 0+  hScrollRatio = (caWidth - ratioBarW) / childWidth+  vScrollRatio = (caHeight - ratioBarW) / childHeight+   hScrollRequired = hScrollRatio < 1   vScrollRequired = vScrollRatio < 1 +  hThumbSize = max minSize (hScrollRatio * vpWidth)+  vThumbSize = max minSize (vScrollRatio * vpHeight)++  hThumbArea = caWidth - ratioBarW+  vThumbArea = caHeight - ratioBarW+  hThumbRatio = (hThumbArea - hThumbSize) / (childWidth - hThumbArea)+  vThumbRatio = (vThumbArea - vThumbSize) / (childHeight - vThumbArea)+   hScrollRect = Rect {     _rX = caLeft,     _rY = caTop + hScrollTop,@@ -857,16 +906,16 @@     _rH = vpHeight   }   hThumbRect = Rect {-    _rX = caLeft - hScrollRatio * dx,+    _rX = caLeft - hThumbRatio * dx,     _rY = caTop + hScrollTop + (barW - thumbW) / 2,-    _rW = hScrollRatio * vpWidth,+    _rW = hThumbSize,     _rH = thumbW   }   vThumbRect = Rect {     _rX = caLeft + vScrollLeft + (barW - thumbW) / 2,-    _rY = caTop - vScrollRatio * dy,+    _rY = caTop - vThumbRatio * dy,     _rW = thumbW,-    _rH = vScrollRatio * vpHeight+    _rH = vThumbSize   }    hMouseInScroll = pointInRect mousePos hScrollRect
src/Monomer/Widgets/Containers/SelectList.hs view
@@ -7,13 +7,22 @@ Portability : non-portable  Select list widget, allowing selection of a single item. List content (rows) is-customizable, plus its styling.+customizable, and so is its styling. This widget is used by+"Monomer.Widgets.Containers.Dropdown" when in its open state.++@+makeRow username = hstack [+    label "User: ",+    label username+  ]++customSelect = selectList userLens usernames makeRow+@ -} {-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE StrictData #-} {-# LANGUAGE ScopedTypeVariables #-} @@ -83,7 +92,7 @@   _slcSelectOnBlur :: Maybe Bool,   _slcItemStyle :: Maybe Style,   _slcItemSelectedStyle :: Maybe Style,-  _slcMergeRequired :: Maybe (Seq a -> Seq a -> Bool),+  _slcMergeRequired :: Maybe (WidgetEnv s e -> Seq a -> Seq a -> Bool),   _slcOnFocusReq :: [Path -> WidgetRequest s e],   _slcOnBlurReq :: [Path -> WidgetRequest s e],   _slcOnChangeReq :: [a -> WidgetRequest s e],@@ -172,7 +181,7 @@     _slcItemSelectedStyle = Just style   } -instance CmbMergeRequired (SelectListCfg s e a) (Seq a) where+instance CmbMergeRequired (SelectListCfg s e a) (WidgetEnv s e) (Seq a) where   mergeRequired fn = def {     _slcMergeRequired = Just fn   }@@ -299,8 +308,8 @@    mergeChildrenReq wenv node oldNode oldState = result where     oldItems = _prevItems oldState-    mergeRequiredFn = fromMaybe (/=) (_slcMergeRequired config)-    result = mergeRequiredFn oldItems items+    mergeRequiredFn = fromMaybe (const (/=)) (_slcMergeRequired config)+    result = mergeRequiredFn wenv oldItems items    merge wenv node oldNode oldState = resultNode newNode where     selected = currentValue wenv@@ -406,7 +415,7 @@    itemScrollTo wenv node idx = maybeToList (scrollToReq <$> mwid <*> vp) where     vp = itemViewport node idx-    mwid = findWidgetIdFromPath wenv (parentPath node)+    mwid = widgetIdFromPath wenv (parentPath node)     scrollToReq wid rect = SendMessage wid (ScrollTo rect)    itemViewport node idx = viewport where
src/Monomer/Widgets/Containers/Split.hs view
@@ -6,9 +6,20 @@ Stability   : experimental Portability : non-portable -Splits the assigned space into two parts, vertically or horizontally, which are+Splits the assigned space into two areas, vertically or horizontally, which are assigned to its two child nodes. The space assigned depends on the style and size requirements of each child node.++@+actionPanel = vstack [+    button "Image 1" ShowImage1,+    button "Image 2" ShowImage2,+    button "Image 3" ShowImage3+  ]+contentPanel = scroll (image activeImage)++mainPanel = hsplit (actionPanel, contentPanel)+@ -} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleContexts #-}
src/Monomer/Widgets/Containers/Stack.hs view
@@ -6,10 +6,35 @@ Stability   : experimental Portability : non-portable -Container which stacks its children along a main axis. The layout algorithm-considers the different type of size requirements and assigns space according to-the logic defined in 'SizeReq'. If the requested fixed space is larger that the-viewport of the stack, the content will overflow.+Container that stacks its children along a main axis.++An hstack widget will assign horizontal space to its children according to their+size requests. The inverse happens with vstack and vertical space, which assigns+vertical space as requested and all the horizontal space available.++For example, a label will get enough space to be displayed completely, and it+will also get all the vertical space the hstack has. This means that if the+hstack is the top level widget in the window, the label will also be as tall as+the window.++Both can be combined to create complex layouts. Considering the situation of a+top-level hstack which created a large vertical label, we could wrap the hstack+with a vstack to only use as much horizontal and vertical space as needed. Both+stack widgets will request space from their parent, along their corresponding+axis, based on the requests of their children.++The layout algorithm considers the different type of size requirements and+assigns space according to the logic defined in 'SizeReq'. If the requested+fixed space is larger that the viewport of the stack, the content will overflow.++@+vstack_ [childSpacing] [+    label "Selected image",+    image "assets/large-image.jpg"+      \`styleBasic\` [maxHeight 400],+    button \"Complete\" CompleteAction+  ]+@ -} {-# LANGUAGE Strict #-} 
src/Monomer/Widgets/Containers/ThemeSwitch.hs view
@@ -8,9 +8,23 @@  Switches to the provided theme for its child nodes. -Note: this widget ignores style settings. If you need to display borders or any-other kind of style configuration, set it on the child node or wrap the-themeSwitch widget in a "Monomer.Widgets.Containers.Box".+@+theme = case activeTheme of+  DarkTheme -> darkTheme+  LightTheme -> lightTheme++widgetTree = themeSwitch theme $ vstack [+    hstack [+      label "Select theme:",+      spacer,+      textDropdownS activeTheme [DarkTheme, LightTheme]+    ]+  ]+@++Note: this widget ignores style settings applied to itself. If you need to+display borders or any other kind of style configuration, set it on the child+node or wrap the themeSwitch widget in a "Monomer.Widgets.Containers.Box". -} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE StrictData #-}
src/Monomer/Widgets/Containers/Tooltip.hs view
@@ -6,13 +6,18 @@ Stability   : experimental Portability : non-portable -Displays a text message above its child node when the pointer is on top and-the delay, if any, has ellapsed.+Displays a text message above its child node when the pointer is on top and the+delay, if any, has ellapsed. -Tooltip styling is a bit unusual, since it only applies to the overlaid element.-This means, padding will not be shown for the contained child element, but only+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 may want to use a box.+element, you can use a "Monomer.Widgets.Containers.Box" around it.++@+tooltip "Click the button" (buttom \"Accept\" AcceptAction)+  \`styleBasic\` [textSize 16, bgColor steelBlue, paddingH 5, radius 5]+@ -} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleContexts #-}@@ -51,7 +56,7 @@ - 'tooltipFollow': if, after tooltip is displayed, it should follow the mouse. -} data TooltipCfg = TooltipCfg {-  _ttcDelay :: Maybe Int,+  _ttcDelay :: Maybe Millisecond,   _ttcFollowCursor :: Maybe Bool,   _ttcMaxWidth :: Maybe Double,   _ttcMaxHeight :: Maybe Double@@ -87,7 +92,7 @@   }  -- | Delay before the tooltip is displayed when child widget is hovered.-tooltipDelay :: Int -> TooltipCfg+tooltipDelay :: Millisecond -> TooltipCfg tooltipDelay ms = def {   _ttcDelay = Just ms }@@ -100,7 +105,7 @@  data TooltipState = TooltipState {   _ttsLastPos :: Point,-  _ttsLastPosTs :: Int+  _ttsLastPosTs :: Millisecond } deriving (Eq, Show, Generic)  -- | Creates a tooltip for the child widget.
src/Monomer/Widgets/Containers/ZStack.hs view
@@ -7,13 +7,21 @@ Portability : non-portable  Layered container, stacking children one on top of the other. Useful for-handling widgets that need to be visible in certain contexts only (dialogs), or-to overlay unrelated widgets (text on top of an image).+handling widgets that need to be visible in certain contexts only, such as+dialogs, or to overlay unrelated widgets (text on top of an image).  The order of the widgets is from bottom to top.  The container will request the largest combination of horizontal and vertical size requested by its child nodes.++@+zstack [+    image_ "assets/test-image.png" [fitFill],+    label "Image caption"+      \`styleBasic\` [textFont \"Bold\", textSize 20, textCenter]+  ]+@ -} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleContexts #-}@@ -127,7 +135,7 @@     ZStackState oldFocusMap oldTopIdx = oldState     children = node ^. L.children     focusedPath = wenv ^. L.focusedPath-    focusedWid = findWidgetIdFromPath wenv focusedPath+    focusedWid = widgetIdFromPath wenv focusedPath     isFocusParent = isNodeParentOfPath node focusedPath      topLevel = isNodeTopLevel wenv node
src/Monomer/Widgets/Singles/Base/InputField.hs view
@@ -10,8 +10,8 @@ representations of other types, such as numbers and dates. It is not meant for direct use, but to create custom widgets using it. -See "Monomer.Widgets.Singles.NumericField", "Monomer.Widgets.Singles.DateField"-and "Monomer.Widgets.Singles.TimeField".+See "Monomer.Widgets.Singles.NumericField", "Monomer.Widgets.Singles.DateField",+"Monomer.Widgets.Singles.TimeField" and "Monomer.Widgets.Singles.TextField". -} {-# LANGUAGE BangPatterns #-} {-# LANGUAGE ConstraintKinds #-}@@ -34,8 +34,7 @@  import Control.Applicative ((<|>)) import Control.Monad-import Control.Lens-  (ALens', (&), (.~), (?~), (%~), (^.), (^?), _2, _Just, cloneLens, non)+import Control.Lens hiding ((|>)) import Data.Default import Data.Maybe import Data.Sequence (Seq(..), (|>))@@ -97,13 +96,15 @@   -- | Caret width.   _ifcCaretWidth :: Maybe Double,   -- | Caret blink period.-  _ifcCaretMs :: Maybe Int,+  _ifcCaretMs :: Maybe Millisecond,   -- | Character to display as text replacement. Useful for passwords.   _ifcDisplayChar :: Maybe Char,   -- | Whether input causes ResizeWidgets requests. Defaults to False.   _ifcResizeOnChange :: Bool,   -- | If all input should be selected when focus is received.   _ifcSelectOnFocus :: Bool,+  -- | Whether the input should be read-only (with editing not allowed, but allowing selection).+  _ifcReadOnly :: Bool,   -- | Conversion from text to the expected value. Failure returns Nothing.   _ifcFromText :: Text -> Maybe a,   -- | Conversion from a value to text. Cannot fail.@@ -184,7 +185,7 @@   -- | Current index into history.   _ifsHistIdx :: Int,   -- | The timestamp when focus was received (used for caret blink)-  _ifsFocusStart :: Int+  _ifsFocusStart :: Millisecond } deriving (Eq, Show, Typeable, Generic)  initialState :: a -> InputFieldState a@@ -207,7 +208,7 @@ defCaretW :: Double defCaretW = 2 -defCaretMs :: Int+defCaretMs :: Millisecond defCaretMs = 500  -- | Creates an instance of an input field, with customizations in config.@@ -255,6 +256,7 @@   -- Text/value conversion functions   !caretW = fromMaybe defCaretW (_ifcCaretWidth config)   !caretMs = fromMaybe defCaretMs (_ifcCaretMs config)+  !editable = not (_ifcReadOnly config)   !fromText = _ifcFromText config   !toText = _ifcToText config   getModelValue !wenv = widgetDataGet (_weModel wenv) (_ifcValue config)@@ -318,10 +320,10 @@     reqs = [ RenderStop widgetId ]    handleKeyPress wenv mod code-    | isDelBackWordNoSel = Just $ moveCursor removeWord prevWordStartIdx Nothing-    | isDelBackWord = Just $ moveCursor removeText minTpSel Nothing-    | isBackspace && emptySel = Just $ moveCursor removeText (tp - 1) Nothing-    | isBackspace = Just $ moveCursor removeText minTpSel Nothing+    | isDelBackWordNoSel && editable = Just $ moveCursor removeWord prevWordStartIdx Nothing+    | isDelBackWord && editable = Just $ moveCursor removeText minTpSel Nothing+    | isBackspace && emptySel && editable = Just $ moveCursor removeText (tp - 1) Nothing+    | isBackspace && editable = Just $ moveCursor removeText minTpSel Nothing     | isMoveLeft = Just $ moveCursor txt (tp - 1) Nothing     | isMoveRight = Just $ moveCursor txt (tp + 1) Nothing     | isMoveWordL = Just $ moveCursor txt prevWordStartIdx Nothing@@ -476,7 +478,8 @@      -- Handle custom drag     Move point-      | isNodePressed wenv node && shiftPressed -> Just result where+      | isNodePressed wenv node && isShiftDrag -> Just result where+        isShiftDrag = shiftPressed && isJust dragHandler         (_, stPoint) = fromJust $ wenv ^. L.mainBtnPress         handlerRes = fromJust dragHandler state stPoint point         (newText, newPos, newSel) = handlerRes@@ -499,11 +502,11 @@     KeyAction mod code KeyPressed       | isKeyboardCopy wenv evt           -> Just $ resultReqs node [SetClipboard (ClipboardText selectedText)]-      | isKeyboardPaste wenv evt+      | isKeyboardPaste wenv evt && editable           -> Just $ resultReqs node [GetClipboard widgetId]-      | isKeyboardCut wenv evt -> cutTextRes wenv node-      | isKeyboardUndo wenv evt -> moveHistory wenv node state config (-1)-      | isKeyboardRedo wenv evt -> moveHistory wenv node state config 1+      | isKeyboardCut wenv evt && editable -> cutTextRes wenv node+      | isKeyboardUndo wenv evt && editable -> moveHistory wenv node state config (-1)+      | isKeyboardRedo wenv evt && editable -> moveHistory wenv node state config 1       | otherwise -> fmap handleKeyRes keyRes <|> cursorRes where           !keyRes = handleKeyPress wenv mod code           handleKeyRes (!newText, !newPos, !newSel) = result where@@ -522,8 +525,9 @@         result = Just (resultReqs node reqs)      -- Text input has unicode already processed (it's not the same as KeyAction)-    TextInput newText -> result where-      result = insertTextRes wenv node newText+    TextInput newText+      | editable -> result where+        result = insertTextRes wenv node newText      -- Paste clipboard contents     Clipboard (ClipboardText newText) -> result where@@ -568,7 +572,7 @@         && shiftPressed         && isJust dragHandler       validCursor-        | not shiftPressed = CursorIBeam+        | not shiftPressed || isNothing dragHandler = CursorIBeam         | otherwise = fromMaybe CursorArrow dragCursor       changeCursorReq newCursor = reqs where         cursorMatch = wenv ^? L.cursor . _Just . _2 == Just newCursor@@ -706,9 +710,14 @@     tsFontColor = styleFontColor style  getCaretH :: InputFieldState a -> Double-getCaretH state = ta - td * 2 where-  TextMetrics ta td _ _ = _ifsTextMetrics state+getCaretH state = lineh where+  TextMetrics asc desc lineh _ = _ifsTextMetrics state +getCaretOffset :: TextMetrics -> StyleState -> Double+getCaretOffset metrics style = textOffset - desc where+  TextMetrics asc desc lineh _ = metrics+  textOffset = textOffsetY metrics style+ getCaretRect   :: InputFieldCfg s e a   -> InputFieldState a@@ -727,7 +736,7 @@     | pos >= length glyphs = _glpXMax (seqLast glyphs)     | otherwise = _glpXMin (Seq.index glyphs pos)   caretX tx = max 0 $ min (cx + cw - caretW) (tx + caretPos)-  caretY = ty + textOffsetY textMetrics style+  caretY = ty + getCaretOffset textMetrics style   caretRect = Rect (caretX tx) caretY caretW (getCaretH state)  getSelRect :: InputFieldState a -> StyleState -> Rect@@ -737,7 +746,7 @@   glyphs = _ifsGlyphs state   pos = _ifsCursorPos state   sel = _ifsSelStart state-  caretY = ty + textOffsetY textMetrics style+  caretY = ty + getCaretOffset textMetrics style   caretH = getCaretH state   glyph idx = Seq.index glyphs (min idx (length glyphs - 1))   gx idx = _glpXMin (glyph idx)@@ -755,7 +764,7 @@   textLen = getGlyphsMax (_ifsGlyphs state)   glyphs     | Seq.null (_ifsGlyphs state) = Seq.empty-    | otherwise = _ifsGlyphs state |> GlyphPos ' ' textLen 0 0 0 0 0+    | otherwise = _ifsGlyphs state |> GlyphPos ' ' 0 textLen 0 0 0 0 0   glyphStart i g = (i, abs (_glpXMin g - localX))   pairs = Seq.mapWithIndex glyphStart glyphs   cpm (_, g1) (_, g2) = compare g1 g2
src/Monomer/Widgets/Singles/Button.hs view
@@ -8,6 +8,10 @@  Button widget, with support for multiline text. At the most basic level, a button consists of a caption and an event to raise when clicked.++@+button "Increase count" AppIncrease+@ -} {-# LANGUAGE BangPatterns #-} {-# LANGUAGE FlexibleContexts #-}@@ -19,10 +23,12 @@   -- * Configuration   ButtonCfg,   -- * Constructors+  mainButton,+  mainButton_,+  mainButtonD_,   button,   button_,-  mainButton,-  mainButton_+  buttonD_ ) where  import Control.Applicative ((<|>))@@ -46,6 +52,9 @@ {-| Configuration options for button: +- 'ignoreParentEvts': whether to ignore all other responses to the click or+  keypress that triggered the button, and only keep this button's response.+  Useful when the button is child of a _keystroke_ widget. - 'trimSpaces': whether to remove leading/trailing spaces in the caption. - 'ellipsis': if ellipsis should be used for overflown text. - 'multiline': if text may be split in multiple lines.@@ -62,6 +71,7 @@ -} data ButtonCfg s e = ButtonCfg {   _btnButtonType :: Maybe ButtonType,+  _btnIgnoreParent :: Maybe Bool,   _btnIgnoreTheme :: Maybe Bool,   _btnLabelCfg :: LabelCfg s e,   _btnOnFocusReq :: [Path -> WidgetRequest s e],@@ -72,6 +82,7 @@ instance Default (ButtonCfg s e) where   def = ButtonCfg {     _btnButtonType = Nothing,+    _btnIgnoreParent = Nothing,     _btnIgnoreTheme = Nothing,     _btnLabelCfg = def,     _btnOnFocusReq = [],@@ -82,6 +93,7 @@ instance Semigroup (ButtonCfg s e) where   (<>) t1 t2 = ButtonCfg {     _btnButtonType = _btnButtonType t2 <|> _btnButtonType t1,+    _btnIgnoreParent = _btnIgnoreParent t2 <|> _btnIgnoreParent t1,     _btnIgnoreTheme = _btnIgnoreTheme t2 <|> _btnIgnoreTheme t1,     _btnLabelCfg = _btnLabelCfg t1 <> _btnLabelCfg t2,     _btnOnFocusReq = _btnOnFocusReq t1 <> _btnOnFocusReq t2,@@ -92,6 +104,11 @@ instance Monoid (ButtonCfg s e) where   mempty = def +instance CmbIgnoreParentEvts (ButtonCfg s e) where+  ignoreParentEvts_ ignore = def {+    _btnIgnoreParent = Just ignore+  }+ instance CmbIgnoreTheme (ButtonCfg s e) where   ignoreTheme_ ignore = def {     _btnIgnoreTheme = Just ignore@@ -165,15 +182,30 @@   _btnButtonType = Just ButtonMain } --- | Creates a button with main styling. Useful for dialogs.+{-|+Creates a button with main styling. Useful to highlight an option, such as+"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 for dialogs. Accepts config.+{-|+Creates a button with main styling. Useful to highlight an option, such as+"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   newConfigs = mainConfig : configs +{-|+Creates a button with main styling. Useful to highlight an option, such as+"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+mainButtonD_ caption configs = buttonD_ caption newConfigs where+  newConfigs = mainConfig : configs+ -- | Creates a button with normal styling. button :: WidgetEvent e => Text -> e -> WidgetNode s e button caption handler = button_ caption handler def@@ -181,7 +213,20 @@ -- | Creates a button with normal styling. Accepts config. button_ :: WidgetEvent e => Text -> e -> [ButtonCfg s e] -> WidgetNode s e button_ caption handler configs = buttonNode where-  config = onClick handler <> mconcat configs+  buttonNode = buttonD_ caption (onClick handler : configs)++{-|+Creates a button without forcing an event to be provided. The other constructors+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'.+-}+buttonD_ :: WidgetEvent e => Text -> [ButtonCfg s e] -> WidgetNode s e+buttonD_ caption configs = buttonNode where+  config = mconcat configs   widget = makeButton caption config   !buttonNode = defaultWidgetNode "button" widget     & L.info . L.focusable .~ True@@ -242,9 +287,12 @@     _ -> Nothing     where       mainBtn btn = btn == wenv ^. L.mainButton+       focused = isNodeFocused wenv node       pointInVp p = isPointInNodeVp node p-      reqs = _btnOnClickReq config+      ignoreParent = _btnIgnoreParent config == Just True++      reqs = _btnOnClickReq config ++ [IgnoreParentEvents | ignoreParent]       result = resultReqs node reqs       resultFocus = resultReqs node [SetFocus (node ^. L.info . L.widgetId)] 
src/Monomer/Widgets/Singles/Checkbox.hs view
@@ -8,10 +8,15 @@  Checkbox widget, used for interacting with boolean values. It does not include text, which can be added with a label in the desired position (usually with-hstack). Alternatively, "Monomer.Widgets.Singles.LabeledCheckbox" provides this-functionality out of the box.+[hstack/vstack]("Monomer.Widgets.Containers.Stack")). Alternatively,+"Monomer.Widgets.Singles.LabeledCheckbox" provides this functionality out of the+box. -'Monomer.Widgets.Singles.ToggleButton' provides similar functionality but with+@+checkbox booleanLens+@++"Monomer.Widgets.Singles.ToggleButton" provides similar functionality but with the look of a regular button. -} {-# LANGUAGE BangPatterns #-}
src/Monomer/Widgets/Singles/ColorPicker.hs view
@@ -6,16 +6,21 @@ Stability   : experimental Portability : non-portable -Color picker using sliders and numeric fields.+Color picker, displayed inside its parent container as a regular widget.++@+colorPicker colorLens+@ -} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE FunctionalDependencies #-}+{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE Strict #-}  module Monomer.Widgets.Singles.ColorPicker (   -- * Configuration   ColorPickerCfg,+  ColorPickerEvt,   -- * Constructors   colorPicker,   colorPicker_,@@ -122,11 +127,12 @@     _cpcOnChangeReq = [req]   } +-- | Internal events for the 'colorPicker' widget. data ColorPickerEvt   = PickerFocus Path   | PickerBlur Path-  | ColorChanged Int-  | AlphaChanged Double+  | PickerColorChanged Int+  | PickerAlphaChanged Double   deriving (Eq, Show)  -- | Creates a color picker using the given lens.@@ -188,8 +194,10 @@ buildUI config wenv model = mainTree where   showAlpha = fromMaybe False (_cpcShowAlpha config)   colorSample = zstack [-      patternImage 2 10 (rgb 255 255 255) (rgb 150 150 150),-      filler `styleBasic` [bgColor model]+      patternImage 2 10 (rgb 255 255 255) (rgb 150 150 150)+        `styleBasic` [radius 4],+      filler+        `styleBasic` [bgColor model, radius 4]     ] `styleBasic` [width 32]    compRow lensCol evt lbl minV maxV = hstack [@@ -201,11 +209,11 @@       spacer_ [width 5],       numericField_ lensCol [minValue minV, maxValue maxV, onChange evt,         onFocus PickerFocus, onBlur PickerBlur]-        `styleBasic` [width 40, padding 0, textRight]+        `styleBasic` [width 40, padding 0, textAscender, textRight]     ] -  colorRow lens lbl = compRow lens ColorChanged lbl 0 255-  alphaRow lens lbl = compRow lens AlphaChanged lbl 0 1+  colorRow lens lbl = compRow lens PickerColorChanged lbl 0 255+  alphaRow lens lbl = compRow lens PickerAlphaChanged lbl 0 1    mainTree = hstack_ [sizeReqUpdater clearExtra] [       vstack [@@ -234,8 +242,8 @@     | not (isNodeParentOfPath node prev) -> reportFocus prev   PickerBlur next     | not (isNodeParentOfPath node next) -> reportBlur next-  ColorChanged _ -> reportChange-  AlphaChanged _ -> reportChange+  PickerColorChanged _ -> reportChange+  PickerAlphaChanged _ -> reportChange   _ -> []   where     report reqs = RequestParent <$> reqs
src/Monomer/Widgets/Singles/DateField.hs view
@@ -6,14 +6,23 @@ Stability   : experimental Portability : non-portable -Input field for dates types.+Input field for dates types with support for valid ranges, different formats and+separators. -Supports the Day type of the <https://hackage.haskell.org/package/time time>-library, but other types can be supported by implementing 'DayConverter'. Maybe-is also supported.+@+dateField dateLens+@ -Supports different date formats and separators.+With configuration options: +@+dateField_ dateLens [dateFormatMMDDYYYY, dateFormatDelimiter \'-\']+@++Supports the 'Day' type of the <https://hackage.haskell.org/package/time time>+library, but other types can be supported by implementing 'DayConverter'.+'Maybe' is also supported.+ Handles mouse wheel and shift + vertical drag to increase/decrease days. -} {-# LANGUAGE ConstraintKinds #-}@@ -25,14 +34,16 @@ module Monomer.Widgets.Singles.DateField (   -- * Configuration   DateFieldCfg,+  DateFieldFormat,   FormattableDate,   DayConverter(..),-  DateTextConverter,+  DateTextConverter(..),   -- * Constructors   dateField,   dateField_,   dateFieldV,   dateFieldV_,+  dateFieldD_,   dateFormatDelimiter,   dateFormatDDMMYYYY,   dateFormatMMDDYYYY,@@ -61,13 +72,14 @@ import qualified Monomer.Lens as L import qualified Monomer.Widgets.Util.Parser as P -data DateFormat+-- | Available formats for 'dateField'.+data DateFieldFormat   = FormatDDMMYYYY   | FormatYYYYMMDD   | FormatMMDDYYYY   deriving (Eq, Show) -defaultDateFormat :: DateFormat+defaultDateFormat :: DateFieldFormat defaultDateFormat = FormatDDMMYYYY  defaultDateDelim :: Char@@ -85,11 +97,14 @@   convertFromDay = id   convertToDay = Just --- | Converts a 'Day' instance to and from 'Text'.+{-|+Converts a 'Day' instance to and from 'Text'. Implementing this typeclass+is not necessary for instances of 'DayConverter'.+-} class DateTextConverter a where-  dateAcceptText :: DateFormat -> Char -> Maybe a -> Maybe a -> Text -> (Bool, Bool, Maybe a)-  dateFromText :: DateFormat -> Char -> Text -> Maybe a-  dateToText :: DateFormat -> Char -> a -> Text+  dateAcceptText :: DateFieldFormat -> Char -> Maybe a -> Maybe a -> Text -> (Bool, Bool, Maybe a)+  dateFromText :: DateFieldFormat -> Char -> Text -> Maybe a+  dateToText :: DateFieldFormat -> Char -> a -> Text   dateFromDay :: Day -> a   dateToDay :: a -> Maybe Day @@ -134,6 +149,7 @@   warnings in the UI, or disable buttons if needed. - 'resizeOnChange': Whether input causes 'ResizeWidgets' requests. - 'selectOnFocus': Whether all input should be selected when focus is received.+- 'readOnly': Whether to prevent the user changing the input text. - 'minValue': Minimum valid date. - 'maxValue': Maximum valid date. - 'wheelRate': The rate at which wheel movement affects the date.@@ -151,17 +167,18 @@ -} data DateFieldCfg s e a = DateFieldCfg {   _dfcCaretWidth :: Maybe Double,-  _dfcCaretMs :: Maybe Int,+  _dfcCaretMs :: Maybe Millisecond,   _dfcValid :: Maybe (WidgetData s Bool),   _dfcValidV :: [Bool -> e],   _dfcDateDelim :: Maybe Char,-  _dfcDateFormat :: Maybe DateFormat,+  _dfcDateFormat :: Maybe DateFieldFormat,   _dfcMinValue :: Maybe a,   _dfcMaxValue :: Maybe a,   _dfcWheelRate :: Maybe Double,   _dfcDragRate :: Maybe Double,   _dfcResizeOnChange :: Maybe Bool,   _dfcSelectOnFocus :: Maybe Bool,+  _dfcReadOnly :: Maybe Bool,   _dfcOnFocusReq :: [Path -> WidgetRequest s e],   _dfcOnBlurReq :: [Path -> WidgetRequest s e],   _dfcOnChangeReq :: [a -> WidgetRequest s e]@@ -181,6 +198,7 @@     _dfcDragRate = Nothing,     _dfcResizeOnChange = Nothing,     _dfcSelectOnFocus = Nothing,+    _dfcReadOnly = Nothing,     _dfcOnFocusReq = [],     _dfcOnBlurReq = [],     _dfcOnChangeReq = []@@ -200,6 +218,7 @@     _dfcDragRate = _dfcDragRate t2 <|> _dfcDragRate t1,     _dfcResizeOnChange = _dfcResizeOnChange t2 <|> _dfcResizeOnChange t1,     _dfcSelectOnFocus = _dfcSelectOnFocus t2 <|> _dfcSelectOnFocus t1,+    _dfcReadOnly = _dfcReadOnly t2 <|> _dfcReadOnly t1,     _dfcOnFocusReq = _dfcOnFocusReq t1 <> _dfcOnFocusReq t2,     _dfcOnBlurReq = _dfcOnBlurReq t1 <> _dfcOnBlurReq t2,     _dfcOnChangeReq = _dfcOnChangeReq t1 <> _dfcOnChangeReq t2@@ -213,7 +232,7 @@     _dfcCaretWidth = Just w   } -instance CmbCaretMs (DateFieldCfg s e a) Int where+instance CmbCaretMs (DateFieldCfg s e a) Millisecond where   caretMs ms = def {     _dfcCaretMs = Just ms   }@@ -238,14 +257,19 @@     _dfcSelectOnFocus = Just sel   } +instance CmbReadOnly (DateFieldCfg s e a) where+  readOnly_ ro = def {+    _dfcReadOnly = Just ro+  }+ instance FormattableDate a => CmbMinValue (DateFieldCfg s e a) a where-  minValue len = def {-    _dfcMinValue = Just len+  minValue value = def {+    _dfcMinValue = Just value   }  instance FormattableDate a => CmbMaxValue (DateFieldCfg s e a) a where-  maxValue len = def {-    _dfcMaxValue = Just len+  maxValue value = def {+    _dfcMaxValue = Just value   }  instance CmbWheelRate (DateFieldCfg s e a) Double where@@ -358,6 +382,7 @@   delim = fromMaybe defaultDateDelim (_dfcDateDelim config)   minVal = _dfcMinValue config   maxVal = _dfcMaxValue config+  readOnly = fromMaybe False (_dfcReadOnly config)    initialValue     | isJust minVal = fromJust minVal@@ -387,9 +412,10 @@     _ifcDisplayChar = Nothing,     _ifcResizeOnChange = fromMaybe False (_dfcResizeOnChange config),     _ifcSelectOnFocus = fromMaybe True (_dfcSelectOnFocus config),+    _ifcReadOnly = readOnly,     _ifcStyle = Just L.dateFieldStyle,-    _ifcWheelHandler = Just (handleWheel config),-    _ifcDragHandler = Just (handleDrag config),+    _ifcWheelHandler = if readOnly then Nothing else Just (handleWheel config),+    _ifcDragHandler =  if readOnly then Nothing else Just (handleDrag config),     _ifcDragCursor = Just CursorSizeV,     _ifcOnFocusReq = _dfcOnFocusReq config,     _ifcOnBlurReq = _dfcOnBlurReq config,@@ -463,7 +489,7 @@  dateFromTextSimple   :: (DayConverter a, FormattableDate a)-  => DateFormat+  => DateFieldFormat   -> Char   -> Text   -> Maybe a@@ -478,7 +504,7 @@       | otherwise -> fromGregorianValid (fromIntegral n1) n2 n3   newDate = tmpDate >>= dateFromDay -dateToTextSimple :: FormattableDate a => DateFormat -> Char -> a -> Text+dateToTextSimple :: FormattableDate a => DateFieldFormat -> Char -> a -> Text dateToTextSimple format delim val = result where   converted = dateToDay val   (year, month, day) = toGregorian (fromJust converted)@@ -495,7 +521,7 @@     | format == FormatMMDDYYYY = tmonth <> sep <> tday <> sep <> tyear     | otherwise = tyear <> sep <> tmonth <> sep <> tday -acceptTextInput :: DateFormat -> Char -> Text -> Bool+acceptTextInput :: DateFieldFormat -> Char -> Text -> Bool acceptTextInput format delim text = isRight (A.parseOnly parser text) where   numP = A.digit *> ""   delimP = A.char delim *> ""
src/Monomer/Widgets/Singles/Dial.hs view
@@ -7,9 +7,14 @@ Portability : non-portable  Dial widget, used for interacting with numeric values. It allows changing the-value by keyboard arrows, dragging the mouse or using the wheel.+value using the keyboard arrows, dragging the mouse or using the wheel. -Similar in objective to "Monomer.Widgets.Singles.Slider", but uses less space.+@+dial numericLens 0 100+@++Similar in objective to "Monomer.Widgets.Singles.Slider", but uses less visual+space in its parent container. -} {-# LANGUAGE BangPatterns #-} {-# LANGUAGE ConstraintKinds #-}
src/Monomer/Widgets/Singles/ExternalLink.hs view
@@ -6,8 +6,12 @@ Stability   : experimental Portability : non-portable -Provides a clickable link that opens in the system's browser. It uses OS-services to open the URI, which means not only URLs can be opened.+Provides a clickable link that is opened by the host OS. Since it relies on the+OS to open the content, it is possible to open URIs other than urls.++@+externalLink "Open Wikipedia" "https://en.wikipedia.org"+@ -} {-# LANGUAGE BangPatterns #-} {-# LANGUAGE FlexibleContexts #-}
src/Monomer/Widgets/Singles/Icon.hs view
@@ -7,6 +7,10 @@ Portability : non-portable  Icon widget. Used for showing basic icons without the need of an asset.++@+icon IconPlus+@ -} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-}
src/Monomer/Widgets/Singles/Image.hs view
@@ -8,6 +8,12 @@  Displays an image from local storage or a url. +@+image "https://picsum.photos/id/1059/800/600"+@++It is also possible to create images from a block of memory using 'imageMem'.+ Notes:  - Depending on the type of image fit chosen and the assigned viewport, some
src/Monomer/Widgets/Singles/Label.hs view
@@ -7,6 +7,18 @@ Portability : non-portable  Label widget, with support for multiline text.++Single line label:++@+label "This is a label"+@++Multi-line label:++@+label_ "This is a\nmultiline label" [multiline]+@ -} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE Strict #-}@@ -142,7 +154,7 @@   _lstTextStyle :: Maybe TextStyle,   _lstTextRect :: Rect,   _lstTextLines :: Seq TextLine,-  _lstPrevResize :: (Int, Bool)+  _lstPrevResize :: (Millisecond, Bool) } deriving (Eq, Show, Generic)  -- | Creates a label using the provided 'Text'.
src/Monomer/Widgets/Singles/LabeledCheckbox.hs view
@@ -6,8 +6,18 @@ Stability   : experimental Portability : non-portable -Labeled checkbox, used for interacting with boolean values with an associated-clickable label.+Labeled checkbox, used for interacting with boolean values. In contrast to+'checkbox', it includes a clickable label.++@+labeledCheckbox "Bool value" booleanLens+@++With text in a different location:++@+labeledCheckbox_ "Checkbox with text below" booleanLens [textBottom]+@ -} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-}
src/Monomer/Widgets/Singles/LabeledRadio.hs view
@@ -6,9 +6,19 @@ Stability   : experimental Portability : non-portable -Radio widget, used for interacting with a fixed set of values with an associated-clickable label. Each instance of the radio will be associated with a single-value.+Labeled radio widget, used for interacting with a fixed set of values. Each+instance of labeled radio is associated with a single value. In contrast to+'radio', it includes a clickable label.++@+labeledRadio "First option" Option1 optionLens+@++With text in a different location:++@+labeledRadio_ "Radio with text below" Option1 optionLens [textBottom]+@ -} {-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE FlexibleInstances #-}
src/Monomer/Widgets/Singles/NumericField.hs view
@@ -6,8 +6,18 @@ Stability   : experimental Portability : non-portable -Input field for numeric types.+Input field for numeric types, with support for valid ranges and decimal places. +@+numericField numericLens+@++With configuration options:++@+numericField_ numericLens [minValue 0, maxValue 100, decimals 2]+@+ Supports instances of the 'FromFractional' typeclass. Several basic types are implemented, both for integer and floating point types. @@ -23,12 +33,13 @@   -- * Configuration   NumericFieldCfg,   FormattableNumber,-  NumericTextConverter,+  NumericTextConverter(..),   -- * Constructors   numericField,   numericField_,   numericFieldV,-  numericFieldV_+  numericFieldV_,+  numericFieldD_ ) where  import Control.Applicative ((<|>))@@ -56,7 +67,10 @@ import qualified Monomer.Lens as L import qualified Monomer.Widgets.Util.Parser as P --- | Converts a numeric instance to and from 'Text'.+{-|+Converts a numeric instance to and from 'Text'. Implementing this typeclass+is not necessary for instances of 'FromFractional'.+-} class NumericTextConverter a where   numericAcceptText :: Maybe a -> Maybe a -> Int -> Text -> (Bool, Bool, Maybe a)   numericFromText :: Text -> Maybe a@@ -107,6 +121,7 @@   warnings in the UI, or disable buttons if needed. - 'resizeOnChange': Whether input causes ResizeWidgets requests. - 'selectOnFocus': Whether all input should be selected when focus is received.+- 'readOnly': Whether to prevent the user changing the input text. - 'minValue': Minimum valid number. - 'maxValue': Maximum valid number. - 'wheelRate': The rate at which wheel movement affects the number.@@ -122,7 +137,7 @@ -} data NumericFieldCfg s e a = NumericFieldCfg {   _nfcCaretWidth :: Maybe Double,-  _nfcCaretMs :: Maybe Int,+  _nfcCaretMs :: Maybe Millisecond,   _nfcValid :: Maybe (WidgetData s Bool),   _nfcValidV :: [Bool -> e],   _nfcDecimals :: Maybe Int,@@ -132,6 +147,7 @@   _nfcDragRate :: Maybe Double,   _nfcResizeOnChange :: Maybe Bool,   _nfcSelectOnFocus :: Maybe Bool,+  _nfcReadOnly :: Maybe Bool,   _nfcOnFocusReq :: [Path -> WidgetRequest s e],   _nfcOnBlurReq :: [Path -> WidgetRequest s e],   _nfcOnChangeReq :: [a -> WidgetRequest s e]@@ -150,6 +166,7 @@     _nfcDragRate = Nothing,     _nfcResizeOnChange = Nothing,     _nfcSelectOnFocus = Nothing,+    _nfcReadOnly = Nothing,     _nfcOnFocusReq = [],     _nfcOnBlurReq = [],     _nfcOnChangeReq = []@@ -167,6 +184,7 @@     _nfcWheelRate = _nfcWheelRate t2 <|> _nfcWheelRate t1,     _nfcDragRate = _nfcDragRate t2 <|> _nfcDragRate t1,     _nfcResizeOnChange = _nfcResizeOnChange t2 <|> _nfcResizeOnChange t1,+    _nfcReadOnly = _nfcReadOnly t2 <|> _nfcReadOnly t1,     _nfcSelectOnFocus = _nfcSelectOnFocus t2 <|> _nfcSelectOnFocus t1,     _nfcOnFocusReq = _nfcOnFocusReq t1 <> _nfcOnFocusReq t2,     _nfcOnBlurReq = _nfcOnBlurReq t1 <> _nfcOnBlurReq t2,@@ -181,7 +199,7 @@     _nfcCaretWidth = Just w   } -instance CmbCaretMs (NumericFieldCfg s e a) Int where+instance CmbCaretMs (NumericFieldCfg s e a) Millisecond where   caretMs ms = def {     _nfcCaretMs = Just ms   }@@ -206,14 +224,19 @@     _nfcSelectOnFocus = Just sel   } +instance CmbReadOnly (NumericFieldCfg s e a) where+  readOnly_ ro = def {+    _nfcReadOnly = Just ro+  }+ instance FormattableNumber a => CmbMinValue (NumericFieldCfg s e a) a where-  minValue len = def {-    _nfcMinValue = Just len+  minValue value = def {+    _nfcMinValue = Just value   }  instance FormattableNumber a => CmbMaxValue (NumericFieldCfg s e a) a where-  maxValue len = def {-    _nfcMaxValue = Just len+  maxValue value = def {+    _nfcMaxValue = Just value   }  instance CmbWheelRate (NumericFieldCfg s e a) Double where@@ -305,14 +328,17 @@   config = mconcat configs   minVal = _nfcMinValue config   maxVal = _nfcMaxValue config+  readOnly = fromMaybe False (_nfcReadOnly config)    initialValue     | isJust minVal = fromJust minVal     | isJust maxVal = fromJust maxVal     | otherwise = numericFromFractional 0-  decimals-    | isIntegral initialValue = 0-    | otherwise = max 0 $ fromMaybe 2 (_nfcDecimals config)+  decimals = case _nfcDecimals config of+    Just count -> max 0 count+    Nothing+      | isIntegral initialValue -> 0+      | otherwise -> 2   defWidth     | isIntegral initialValue = 50     | otherwise = 70@@ -340,9 +366,10 @@     _ifcDisplayChar = Nothing,     _ifcResizeOnChange = fromMaybe False (_nfcResizeOnChange config),     _ifcSelectOnFocus = fromMaybe True (_nfcSelectOnFocus config),+    _ifcReadOnly = readOnly,     _ifcStyle = Just L.numericFieldStyle,-    _ifcWheelHandler = Just (handleWheel config),-    _ifcDragHandler = Just (handleDrag config),+    _ifcWheelHandler = if readOnly then Nothing else Just (handleWheel config),+    _ifcDragHandler = if readOnly then Nothing else Just (handleDrag config),     _ifcDragCursor = Just CursorSizeV,     _ifcOnFocusReq = _nfcOnFocusReq config,     _ifcOnBlurReq = _nfcOnBlurReq config,@@ -438,7 +465,6 @@ isIntegral :: Typeable a => a -> Bool isIntegral val   | "Int" `isPrefixOf` name = True-  | "Fixed" `isPrefixOf` name = True   | "Word" `isPrefixOf` name = True   | otherwise = False   where
src/Monomer/Widgets/Singles/OptionButton.hs view
@@ -7,27 +7,32 @@ Portability : non-portable  Option button widget, used for choosing one value from a fixed set. Each-instance of optionButton will be associated with a single value.+instance of optionButton is associated with a single value. -Its behavior is equivalent to 'Monomer.Widgets.Singles.Radio' and-'Monomer.Widgets.Singles.LabeledRadio', with a different visual representation.+@+optionButton "First option" Option1 optionLens+@ -This widget, and the associated 'ToggleButton', uses two separate styles for the-On and Off states which can be modified individually for the theme. If you use-any of the the standard style functions (styleBasic, styleHover, etc) in an-optionButton/toggleButton these changes will apply to both On and Off states,-except for the color related styles. The reason for this is that, in general,-you will want to use the same font and padding for both states, but colors will-usually differ. For changing the colors of the Off state you can use-'optionButtonOffStyle', that receives a 'Style' instance. The values set here-are higher priority than any inherited style from the theme or node text style.+Its behavior is equivalent to "Monomer.Widgets.Singles.Radio" and+"Monomer.Widgets.Singles.LabeledRadio", with a different visual representation. +This widget, and the associated "Monomer.Widgets.Singles.ToggleButton", uses two+separate styles for the On and Off states which can be modified individually for+the theme. If you use any of the the standard style functions (styleBasic,+styleHover, etc) in an optionButton/toggleButton these changes will apply to+both On and Off states, except for the color related styles. The reason is that,+in general, the font and padding will be the same for both states, but the+colors will differ. The 'optionButtonOffStyle' option, which receives a 'Style'+instance, can be used to change the colors of the Off state. The values set with+this option are higher priority than any inherited style from the theme or node+text style.+ 'Style' instances can be created this way:  @ newStyle :: Style = def-  `styleBasic` [textSize 20]-  `styleHover` [textColor white]+  \`styleBasic\` [textSize 20]+  \`styleHover\` [textColor white] @ -} {-# LANGUAGE BangPatterns #-}@@ -88,8 +93,10 @@ - 'onFocusReq': 'WidgetRequest' to generate when focus is received. - 'onBlur': event to raise when focus is lost. - 'onBlurReq': 'WidgetRequest' to generate when focus is lost.-- 'onChange': event to raise when the value changes/is clicked.-- 'onChangeReq': 'WidgetRequest' to generate when the value changes/is clicked.+- 'onClick': event to raise when the value is clicked.+- 'onClickReq': 'WidgetRequest' to generate when the value is clicked.+- 'onChange': event to raise when the value changes.+- 'onChangeReq': 'WidgetRequest' to generate when the value changes. -} data OptionButtonCfg s e a = OptionButtonCfg {   _obcIgnoreTheme :: Maybe Bool,@@ -97,6 +104,7 @@   _obcLabelCfg :: LabelCfg s e,   _obcOnFocusReq :: [Path -> WidgetRequest s e],   _obcOnBlurReq :: [Path -> WidgetRequest s e],+  _obcOnClickReq :: [WidgetRequest s e],   _obcOnChangeReq :: [a -> WidgetRequest s e] } @@ -107,6 +115,7 @@     _obcLabelCfg = def,     _obcOnFocusReq = [],     _obcOnBlurReq = [],+    _obcOnClickReq = [],     _obcOnChangeReq = []   } @@ -117,6 +126,7 @@     _obcLabelCfg = _obcLabelCfg t1 <> _obcLabelCfg t2,     _obcOnFocusReq = _obcOnFocusReq t1 <> _obcOnFocusReq t2,     _obcOnBlurReq = _obcOnBlurReq t1 <> _obcOnBlurReq t2,+    _obcOnClickReq = _obcOnClickReq t1 <> _obcOnClickReq t2,     _obcOnChangeReq = _obcOnChangeReq t1 <> _obcOnChangeReq t2   } @@ -181,6 +191,16 @@     _obcOnBlurReq = [req]   } +instance WidgetEvent e => CmbOnClick (OptionButtonCfg s e a) e where+  onClick req = def {+    _obcOnClickReq = [RaiseEvent req]+  }++instance CmbOnClickReq (OptionButtonCfg s e a) s e where+  onClickReq req = def {+    _obcOnClickReq = [req]+  }+ instance WidgetEvent e => CmbOnChange (OptionButtonCfg s e a) a e where   onChange fn = def {     _obcOnChangeReq = [RaiseEvent . fn]@@ -259,6 +279,10 @@   optionButtonNode = defaultWidgetNode wtype widget     & L.info . L.focusable .~ True +{-|+Helper function for creating a button associated to a value. Used by+_optionButton_ and _toggleButton_.+-} makeOptionButton   :: OptionButtonValue a   => Lens' ThemeState StyleState@@ -339,7 +363,11 @@       currValue = widgetDataGet (wenv ^. L.model) field       nextValue = getNextVal currValue       setValueReq = widgetDataSet field nextValue-      reqs = setValueReq ++ fmap ($ nextValue) (_obcOnChangeReq config)+      clickReqs = _obcOnClickReq config+      changeReqs+        | currValue /= nextValue = fmap ($ nextValue) (_obcOnChangeReq config)+        | otherwise = []+      reqs = setValueReq ++ clickReqs ++ changeReqs       result = resultReqs node reqs       resultFocus = resultReqs node [SetFocus (node ^. L.info . L.widgetId)] 
src/Monomer/Widgets/Singles/Radio.hs view
@@ -7,12 +7,17 @@ Portability : non-portable  Radio widget, used for interacting with a fixed set of values. Each instance of-the radio will be associated with a single value. It does not include text,-which should be added as a label in the desired position (usually with hstack).-Alternatively, 'Monomer.Widgets.Singles.LabeledRadio' provides this-functionality out of the box.+the radio is associated with a single value. It does not include text, which can+be added with a label in the desired position (usually with+[hstack/vstack]("Monomer.Widgets.Containers.Stack")). Alternatively,+"Monomer.Widgets.Singles.LabeledRadio" provides this functionality out of the+box. -'Monomer.Widgets.Singles.OptionButton' provides similar functionality but with+@+radio Option1 optionLens+@++"Monomer.Widgets.Singles.OptionButton" provides similar functionality but with the look of a regular button. -} {-# LANGUAGE BangPatterns #-}@@ -57,13 +62,16 @@ - 'onFocusReq': 'WidgetRequest' to generate when focus is received. - 'onBlur': event to raise when focus is lost. - 'onBlurReq': 'WidgetRequest' to generate when focus is lost.-- 'onChange': event to raise when the value changes/is clicked.-- 'onChangeReq': 'WidgetRequest' to generate when the value changes/is clicked.+- 'onClick': event to raise when the value is clicked.+- 'onClickReq': 'WidgetRequest' to generate when the value is clicked.+- 'onChange': event to raise when the value changes.+- 'onChangeReq': 'WidgetRequest' to generate when the value changes. -} data RadioCfg s e a = RadioCfg {   _rdcWidth :: Maybe Double,   _rdcOnFocusReq :: [Path -> WidgetRequest s e],   _rdcOnBlurReq :: [Path -> WidgetRequest s e],+  _rdcOnClickReq :: [WidgetRequest s e],   _rdcOnChangeReq :: [a -> WidgetRequest s e] } @@ -72,6 +80,7 @@     _rdcWidth = Nothing,     _rdcOnFocusReq = [],     _rdcOnBlurReq = [],+    _rdcOnClickReq = [],     _rdcOnChangeReq = []   } @@ -80,6 +89,7 @@     _rdcWidth = _rdcWidth t2 <|> _rdcWidth t1,     _rdcOnFocusReq = _rdcOnFocusReq t1 <> _rdcOnFocusReq t2,     _rdcOnBlurReq = _rdcOnBlurReq t1 <> _rdcOnBlurReq t2,+    _rdcOnClickReq = _rdcOnClickReq t1 <> _rdcOnClickReq t2,     _rdcOnChangeReq = _rdcOnChangeReq t1 <> _rdcOnChangeReq t2   } @@ -111,6 +121,16 @@     _rdcOnBlurReq = [req]   } +instance WidgetEvent e => CmbOnClick (RadioCfg s e a) e where+  onClick fn = def {+    _rdcOnClickReq = [RaiseEvent fn]+  }++instance CmbOnClickReq (RadioCfg s e a) s e where+  onClickReq req = def {+    _rdcOnClickReq = [req]+  }+ instance WidgetEvent e => CmbOnChange (RadioCfg s e a) a e where   onChange fn = def {     _rdcOnChangeReq = [RaiseEvent . fn]@@ -208,8 +228,14 @@       rdArea = getRadioArea wenv node config       path = node ^. L.info . L.path       isSelectKey code = isKeyReturn code || isKeySpace code++      currValue = widgetDataGet (wenv ^. L.model) field       setValueReq = widgetDataSet field option-      reqs = setValueReq ++ fmap ($ option) (_rdcOnChangeReq config)+      clickReqs = _rdcOnClickReq config+      changeReqs+        | currValue /= option = fmap ($ option) (_rdcOnChangeReq config)+        | otherwise = []+      reqs = setValueReq ++ clickReqs ++ changeReqs    getSizeReq wenv node = req where     theme = currentTheme wenv node
src/Monomer/Widgets/Singles/SeparatorLine.hs view
@@ -10,8 +10,16 @@ to the active layout direction, creating a vertical line on a horizontal layout and viceversa. -The line has the provided width in the direction orthogonal to the layout-direction, and takes all the available space in the other direction. In case of+@+hstack [+  label "Left half",+  separatorLine,+  label "Right half"+]+@++The separator line has the provided width in the direction orthogonal to the+parent layout, and takes all the available space in the other axis. In case of wanting a shorter line, padding should be used. -} {-# LANGUAGE BangPatterns #-}
src/Monomer/Widgets/Singles/Slider.hs view
@@ -7,9 +7,13 @@ Portability : non-portable  Slider widget, used for interacting with numeric values. It allows changing the-value by keyboard arrows, dragging the mouse or using the wheel.+value using the keyboard arrows, dragging the mouse or using the wheel. -Similar in objective to 'Monomer.Widgets.Singles.Dial', but more convenient in+@+hslider numericLens 0 100+@++Similar in objective to "Monomer.Widgets.Singles.Dial", but more convenient in some layouts. -} {-# LANGUAGE BangPatterns #-}
src/Monomer/Widgets/Singles/Spacer.hs view
@@ -7,10 +7,27 @@ Portability : non-portable  Spacer is used for adding a fixed space between two widgets.++@+hstack [+  label \"Username\",+  spacer,+  label username+]+@+ Filler is used for taking all the unused space between two widgets. Useful for alignment purposes. -Both adapt to the current layout direction, if any.+@+hstack [+  label "Section title",+  filler,+  button \"Close\" CloseSection+]+@++Both adapt to the active layout direction. -} {-# LANGUAGE FlexibleContexts #-} 
src/Monomer/Widgets/Singles/TextArea.hs view
@@ -6,7 +6,18 @@ Stability   : experimental Portability : non-portable -Input field for multiline 'Text'.+Input field for multiline 'Text'. Allows setting the maximum number of+characters, lines and whether the tab key should trigger focus change.++@+textArea longTextLens+@++With configuration options:++@+textArea_ longTextLens [maxLength 1000, selectOnFocus]+@ -} {-# LANGUAGE BangPatterns #-} {-# LANGUAGE ConstraintKinds #-}@@ -23,11 +34,12 @@   textArea,   textArea_,   textAreaV,-  textAreaV_+  textAreaV_,+  textAreaD_ ) where  import Control.Applicative ((<|>))-import Control.Lens ((&), (^.), (^?), (.~), (%~), (<>~), ALens', ix, view)+import Control.Lens hiding ((|>)) import Control.Monad (forM_, when) import Data.Default import Data.Foldable (toList)@@ -49,7 +61,7 @@ defCaretW :: Double defCaretW = 2 -defCaretMs :: Int+defCaretMs :: Millisecond defCaretMs = 500  {-|@@ -60,6 +72,7 @@ - 'acceptTab': whether to handle tab and convert it to spaces (cancelling change   of focus), or keep default behaviour and lose focus. - 'selectOnFocus': Whether all input should be selected when focus is received.+- 'readOnly': Whether to prevent the user changing the input text. - 'onFocus': event to raise when focus is received. - 'onFocusReq': 'WidgetRequest' to generate when focus is received. - 'onBlur': event to raise when focus is lost.@@ -69,11 +82,12 @@ -} data TextAreaCfg s e = TextAreaCfg {   _tacCaretWidth :: Maybe Double,-  _tacCaretMs :: Maybe Int,+  _tacCaretMs :: Maybe Millisecond,   _tacMaxLength :: Maybe Int,   _tacMaxLines :: Maybe Int,   _tacAcceptTab :: Maybe Bool,   _tacSelectOnFocus :: Maybe Bool,+  _tacReadOnly :: Maybe Bool,   _tacOnFocusReq :: [Path -> WidgetRequest s e],   _tacOnBlurReq :: [Path -> WidgetRequest s e],   _tacOnChangeReq :: [Text -> WidgetRequest s e]@@ -87,6 +101,7 @@     _tacMaxLines = Nothing,     _tacAcceptTab = Nothing,     _tacSelectOnFocus = Nothing,+    _tacReadOnly = Nothing,     _tacOnFocusReq = [],     _tacOnBlurReq = [],     _tacOnChangeReq = []@@ -100,6 +115,7 @@     _tacMaxLines = _tacMaxLines t2 <|> _tacMaxLines t1,     _tacAcceptTab = _tacAcceptTab t2 <|> _tacAcceptTab t1,     _tacSelectOnFocus = _tacSelectOnFocus t2 <|> _tacSelectOnFocus t1,+    _tacReadOnly = _tacReadOnly t2 <|> _tacReadOnly t1,     _tacOnFocusReq = _tacOnFocusReq t1 <> _tacOnFocusReq t2,     _tacOnBlurReq = _tacOnBlurReq t1 <> _tacOnBlurReq t2,     _tacOnChangeReq = _tacOnChangeReq t1 <> _tacOnChangeReq t2@@ -113,7 +129,7 @@     _tacCaretWidth = Just w   } -instance CmbCaretMs (TextAreaCfg s e) Int where+instance CmbCaretMs (TextAreaCfg s e) Millisecond where   caretMs ms = def {     _tacCaretMs = Just ms   }@@ -138,6 +154,11 @@     _tacSelectOnFocus = Just sel   } +instance CmbReadOnly (TextAreaCfg s e) where+  readOnly_ ro = def {+    _tacReadOnly = Just ro+  }+ instance WidgetEvent e => CmbOnFocus (TextAreaCfg s e) e Path where   onFocus fn = def {     _tacOnFocusReq = [RaiseEvent . fn]@@ -183,7 +204,7 @@   _tasTextLines :: Seq TextLine,   _tasHistory :: Seq HistoryStep,   _tasHistoryIdx :: Int,-  _tasFocusStart :: Int+  _tasFocusStart :: Millisecond } deriving (Eq, Show, Generic)  instance Default TextAreaState where@@ -254,6 +275,7 @@   !caretMs = fromMaybe defCaretMs (_tacCaretMs config)   !maxLength = _tacMaxLength config   !maxLines = _tacMaxLines config+  !editable = _tacReadOnly config /= Just True   getModelValue !wenv = widgetDataGet (_weModel wenv) wdata   -- State   !currText = _tasText state@@ -291,10 +313,10 @@     reqs = [RenderStop widgetId]    handleKeyPress wenv mod code-    | isDelBackWordNoSel = Just removeWordL-    | isDelBackWord = Just (replaceText state selStart "")-    | isBackspace && emptySel = Just removeCharL-    | isBackspace = Just (replaceText state selStart "")+    | isDelBackWordNoSel && editable = Just removeWordL+    | isDelBackWord && editable = Just (replaceText state selStart "")+    | isBackspace && emptySel && editable = Just removeCharL+    | isBackspace && editable = Just (replaceText state selStart "")     | isMoveLeft = Just $ moveCursor txt (tpX - 1, tpY) Nothing     | isMoveRight = Just $ moveCursor txt (tpX + 1, tpY) Nothing     | isMoveUp = Just $ moveCursor txt (tpX, tpY - 1) Nothing@@ -534,12 +556,12 @@      KeyAction mod code KeyPressed       | isKeyboardCopy wenv evt -> Just resultCopy-      | isKeyboardPaste wenv evt -> Just resultPaste-      | isKeyboardCut wenv evt -> Just resultCut-      | isKeyboardUndo wenv evt -> Just $ moveHistory bwdState (-1)-      | isKeyboardRedo wenv evt -> Just $ moveHistory state 1-      | isKeyReturn code -> Just resultReturn-      | isKeyTab code && acceptTab -> Just resultTab+      | isKeyboardPaste wenv evt && editable -> Just resultPaste+      | isKeyboardCut wenv evt && editable -> Just resultCut+      | isKeyboardUndo wenv evt && editable -> Just $ moveHistory bwdState (-1)+      | isKeyboardRedo wenv evt && editable -> Just $ moveHistory state 1+      | isKeyReturn code && editable -> Just resultReturn+      | isKeyTab code && acceptTab && editable -> Just resultTab       | otherwise -> fmap handleKeyRes (handleKeyPress wenv mod code)       where         acceptTab = fromMaybe False (_tacAcceptTab config)@@ -576,8 +598,9 @@             & L.widget .~ makeTextArea wdata config newState           result = resultReqs newNode (generateReqs wenv node newState) -    TextInput newText -> Just result where-      result = insertText wenv node newText+    TextInput newText+      | editable -> Just result where+        result = insertText wenv node newText      Clipboard (ClipboardText newText) -> Just result where       result = insertText wenv node newText@@ -644,10 +667,10 @@   generateScrollReq wenv node newState = scrollReq where     style = currentStyle wenv node     scPath = parentPath node-    scWid = findWidgetIdFromPath wenv scPath+    scWid = widgetIdFromPath wenv scPath     contentArea = getContentArea node style     offset = Point (contentArea ^. L.x) (contentArea ^. L.y)-    caretRect = getCaretRect config newState True+    caretRect = getCaretRect config newState     -- Padding/border added to show left/top borders when moving near them     scrollRect = fromMaybe caretRect (addOuterBounds style caretRect)     scrollMsg = ScrollTo $ moveRect offset scrollRect@@ -683,43 +706,41 @@       caretTs = ts - _tasFocusStart state       caretRequired = focused && even (caretTs `div` caretMs)       caretColor = styleFontColor style-      caretRect = getCaretRect config state False+      caretRect = getCaretRect config state        selRequired = isJust (_tasSelStart state)       selColor = styleHlColor style       selRects = getSelectionRects state contentArea -getCaretRect :: TextAreaCfg s e -> TextAreaState -> Bool -> Rect-getCaretRect config state addSpcV = caretRect where-  caretW = fromMaybe defCaretW (_tacCaretWidth config)+getCaretRect :: TextAreaCfg s e -> TextAreaState -> Rect+getCaretRect config state = caretRect where   (cursorX, cursorY) = _tasCursorPos state-  TextMetrics _ _ lineh _ = _tasTextMetrics state+  Rect tx ty _ _ = lineRect+  TextMetrics asc desc lineh _ = _tasTextMetrics state   textLines = _tasTextLines state -  (lineRect, glyphs, spaceV) = case Seq.lookup cursorY textLines of+  (lineRect, glyphs, fspaceV) = case Seq.lookup cursorY textLines of     Just tl -> (tl ^. L.rect, tl ^. L.glyphs, tl ^. L.fontSpaceV)     Nothing -> (def, Seq.empty, def)--  Rect tx ty _ _ = lineRect-  totalH-    | addSpcV = lineh + unFontSpace spaceV-    | otherwise = lineh+  spaceV = unFontSpace fspaceV    caretPos     | cursorX == 0 || cursorX > length glyphs = 0     | cursorX == length glyphs = _glpXMax (Seq.index glyphs (cursorX - 1))     | otherwise = _glpXMin (Seq.index glyphs cursorX)+   caretX = max 0 (tx + caretPos)-  caretY-    | cursorY == length textLines = fromIntegral cursorY * totalH-    | otherwise = ty-  caretRect = Rect caretX caretY caretW totalH+  caretY = ty - spaceV+  caretW = fromMaybe defCaretW (_tacCaretWidth config)+  caretH = lineh + spaceV +  caretRect = Rect caretX caretY caretW caretH+ getSelectionRects :: TextAreaState -> Rect -> [Rect] getSelectionRects state contentArea = rects where   currPos = _tasCursorPos state   currSel = fromMaybe def (_tasSelStart state)-  TextMetrics _ _ lineh _ = _tasTextMetrics state+  TextMetrics asc desc lineh _ = _tasTextMetrics state   textLines = _tasTextLines state    spaceV = getSpaceV textLines@@ -740,14 +761,16 @@     | swap currPos <= swap currSel = (currPos, currSel)     | otherwise = (currSel, currPos) +  totalH = lineh + spaceV   updateRect rect = rect-    & L.h .~ lineh + spaceV+    & L.y -~ spaceV+    & L.h .~ totalH     & L.w %~ max 5 -- Empty lines show a small rect to indicate they are there.   makeRect cx1 cx2 cy = Rect rx ry rw rh where     rx = glyphPos cx1 cy     rw = glyphPos cx2 cy - rx-    ry = fromIntegral cy * (lineh + spaceV)-    rh = lineh + spaceV+    ry = fromIntegral cy * totalH - spaceV+    rh = totalH   rects     | selY1 == selY2 = [makeRect selX1 selX2 selY1]     | otherwise = begin : middle ++ end where@@ -907,7 +930,7 @@    glyphs     | Seq.null lineGlyphs = Seq.empty-    | otherwise = lineGlyphs |> GlyphPos ' ' textLen 0 0 0 0 0+    | otherwise = lineGlyphs |> GlyphPos ' ' 0 textLen 0 0 0 0 0   glyphStart i g = (i, abs (_glpXMin g - x))    pairs = Seq.mapWithIndex glyphStart glyphs
src/Monomer/Widgets/Singles/TextDropdown.hs view
@@ -7,8 +7,14 @@ Portability : non-portable  Dropdown widget, allowing selection of a single item from a collapsable list.-Both header and list content is text based. In case a customizable version is-is needed, 'Monomer.Widgets.Containers.Dropdown' can be used.+Both header and list content are text based.++@+textDropdown textLens ["Option 1", "Option 2", "Option 3"]+@++In case a customizable version is needed, to display rich content in the header+or list items, "Monomer.Widgets.Containers.Dropdown" can be used. -} {-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE Strict #-}
src/Monomer/Widgets/Singles/TextField.hs view
@@ -6,7 +6,19 @@ Stability   : experimental Portability : non-portable -Input field for single line 'Text'.+Input field for single line 'Text'. Allows setting the maximum number of+characters and a replacement character for password.++@+textField shortTextLens+@++With configuration options:++@+textField_ shortTextLens [maxLength 100, selectOnFocus_ False]+@+ -} {-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE FlexibleContexts #-}@@ -47,6 +59,7 @@   warnings in the UI, or disable buttons if needed. - 'resizeOnChange': Whether input causes ResizeWidgets requests. - 'selectOnFocus': Whether all input should be selected when focus is received.+- 'readOnly': Whether to prevent the user changing the input text. - 'maxLength': the maximum length of input text. - 'textFieldDisplayChar': the character that will be displayed as replacement of   the real text. Useful for password fields.@@ -59,7 +72,7 @@ -} data TextFieldCfg s e = TextFieldCfg {   _tfcCaretWidth :: Maybe Double,-  _tfcCaretMs :: Maybe Int,+  _tfcCaretMs :: Maybe Millisecond,   _tfcDisplayChar :: Maybe Char,   _tfcPlaceholder :: Maybe Text,   _tfcValid :: Maybe (WidgetData s Bool),@@ -67,6 +80,7 @@   _tfcMaxLength :: Maybe Int,   _tfcResizeOnChange :: Maybe Bool,   _tfcSelectOnFocus :: Maybe Bool,+  _tfcReadOnly :: Maybe Bool,   _tfcOnFocusReq :: [Path -> WidgetRequest s e],   _tfcOnBlurReq :: [Path -> WidgetRequest s e],   _tfcOnChangeReq :: [Text -> WidgetRequest s e]@@ -83,6 +97,7 @@     _tfcMaxLength = Nothing,     _tfcResizeOnChange = Nothing,     _tfcSelectOnFocus = Nothing,+    _tfcReadOnly = Nothing,     _tfcOnFocusReq = [],     _tfcOnBlurReq = [],     _tfcOnChangeReq = []@@ -99,6 +114,7 @@     _tfcMaxLength = _tfcMaxLength t2 <|> _tfcMaxLength t1,     _tfcResizeOnChange = _tfcResizeOnChange t2 <|> _tfcResizeOnChange t1,     _tfcSelectOnFocus = _tfcSelectOnFocus t2 <|> _tfcSelectOnFocus t1,+    _tfcReadOnly = _tfcReadOnly t2 <|> _tfcReadOnly t1,     _tfcOnFocusReq = _tfcOnFocusReq t1 <> _tfcOnFocusReq t2,     _tfcOnBlurReq = _tfcOnBlurReq t1 <> _tfcOnBlurReq t2,     _tfcOnChangeReq = _tfcOnChangeReq t1 <> _tfcOnChangeReq t2@@ -112,7 +128,7 @@     _tfcCaretWidth = Just w   } -instance CmbCaretMs (TextFieldCfg s e) Int where+instance CmbCaretMs (TextFieldCfg s e) Millisecond where   caretMs ms = def {     _tfcCaretMs = Just ms   }@@ -142,6 +158,11 @@     _tfcSelectOnFocus = Just sel   } +instance CmbReadOnly (TextFieldCfg s e) where+  readOnly_ ro = def {+    _tfcReadOnly = Just ro+  }+ instance CmbMaxLength (TextFieldCfg s e) where   maxLength len = def {     _tfcMaxLength = Just len@@ -227,6 +248,7 @@     _ifcDisplayChar = _tfcDisplayChar config,     _ifcResizeOnChange = fromMaybe False (_tfcResizeOnChange config),     _ifcSelectOnFocus = fromMaybe False (_tfcSelectOnFocus config),+    _ifcReadOnly = fromMaybe False (_tfcReadOnly config),     _ifcStyle = Just L.textFieldStyle,     _ifcWheelHandler = Nothing,     _ifcDragHandler = Nothing,
src/Monomer/Widgets/Singles/TimeField.hs view
@@ -6,14 +6,22 @@ Stability   : experimental Portability : non-portable -Input field for time types.+Input field for time types with support for different formats. -Supports TimeOfDay type of the <https://hackage.haskell.org/package/time time>-library, but other types can be supported by implementing 'TimeOfDayConverter'.-Maybe is also supported.+@+timeField timeLens+@ -Supports different time formats.+With configuration options: +@+timeField_ timeLens [timeFormatHHMMSS]+@++Supports 'TimeOfDay' type of the <https://hackage.haskell.org/package/time time>+library, but other types can be supported by implementing 'TimeOfDayConverter'.+'Maybe' is also supported.+ Handles mouse wheel and shift + vertical drag to increase/decrease minutes. -} {-# LANGUAGE ConstraintKinds #-}@@ -26,12 +34,15 @@   -- * Configuration   TimeFieldCfg,   FormattableTime,+  TimeFieldFormat,   TimeOfDayConverter(..),+  TimeTextConverter(..),   -- * Constructors   timeField,   timeField_,   timeFieldV,   timeFieldV_,+  timeFieldD_,   timeFormatHHMM,   timeFormatHHMMSS ) where@@ -58,12 +69,13 @@ import qualified Monomer.Lens as L import qualified Monomer.Widgets.Util.Parser as P -data TimeFormat+-- | Available formats for 'timeField'.+data TimeFieldFormat   = FormatHHMM   | FormatHHMMSS   deriving (Eq, Show) -defaultTimeFormat :: TimeFormat+defaultTimeFormat :: TimeFieldFormat defaultTimeFormat = FormatHHMM  defaultTimeDelim :: Char@@ -81,11 +93,14 @@   convertFromTimeOfDay = id   convertToTimeOfDay = Just --- | Converts a 'TimeOfDay' instance to and from 'Text'.+{-|+Converts a 'TimeOfDay' instance to and from 'Text'. Implementing this typeclass+is not necessary for instances of 'TimeOfDayConverter'.+-} class TimeTextConverter a where-  timeAcceptText :: TimeFormat -> Maybe a -> Maybe a -> Text -> (Bool, Bool, Maybe a)-  timeFromText :: TimeFormat -> Text -> Maybe a-  timeToText :: TimeFormat -> a -> Text+  timeAcceptText :: TimeFieldFormat -> Maybe a -> Maybe a -> Text -> (Bool, Bool, Maybe a)+  timeFromText :: TimeFieldFormat -> Text -> Maybe a+  timeToText :: TimeFieldFormat -> a -> Text   timeFromTimeOfDay' :: TimeOfDay -> a   timeToTimeOfDay' :: a -> Maybe TimeOfDay @@ -130,10 +145,11 @@ warnings in the UI, or disable buttons if needed. - 'resizeOnChange': Whether input causes ResizeWidgets requests. - 'selectOnFocus': Whether all input should be selected when focus is received.-- 'minValue': Minimum valid date.-- 'maxValue': Maximum valid date.-- 'wheelRate': The rate at which wheel movement affects the date.-- 'dragRate': The rate at which drag movement affects the date.+- 'readOnly': Whether to prevent the user changing the input text.+- 'minValue': Minimum valid time.+- 'maxValue': Maximum valid time.+- 'wheelRate': The rate at which wheel movement affects the time.+- 'dragRate': The rate at which drag movement affects the time. - 'onFocus': event to raise when focus is received. - 'onFocusReq': 'WidgetRequest' to generate when focus is received. - 'onBlur': event to raise when focus is lost.@@ -145,16 +161,17 @@ -} data TimeFieldCfg s e a = TimeFieldCfg {   _tfcCaretWidth :: Maybe Double,-  _tfcCaretMs :: Maybe Int,+  _tfcCaretMs :: Maybe Millisecond,   _tfcValid :: Maybe (WidgetData s Bool),   _tfcValidV :: [Bool -> e],-  _tfcTimeFormat :: Maybe TimeFormat,+  _tfcTimeFormat :: Maybe TimeFieldFormat,   _tfcMinValue :: Maybe a,   _tfcMaxValue :: Maybe a,   _tfcWheelRate :: Maybe Double,   _tfcDragRate :: Maybe Double,   _tfcResizeOnChange :: Maybe Bool,   _tfcSelectOnFocus :: Maybe Bool,+  _tfcReadOnly :: Maybe Bool,   _tfcOnFocusReq :: [Path -> WidgetRequest s e],   _tfcOnBlurReq :: [Path -> WidgetRequest s e],   _tfcOnChangeReq :: [a -> WidgetRequest s e]@@ -173,6 +190,7 @@     _tfcDragRate = Nothing,     _tfcResizeOnChange = Nothing,     _tfcSelectOnFocus = Nothing,+    _tfcReadOnly = Nothing,     _tfcOnFocusReq = [],     _tfcOnBlurReq = [],     _tfcOnChangeReq = []@@ -191,6 +209,7 @@     _tfcDragRate = _tfcDragRate t2 <|> _tfcDragRate t1,     _tfcResizeOnChange = _tfcResizeOnChange t2 <|> _tfcResizeOnChange t1,     _tfcSelectOnFocus = _tfcSelectOnFocus t2 <|> _tfcSelectOnFocus t1,+    _tfcReadOnly = _tfcReadOnly t2 <|> _tfcReadOnly t1,     _tfcOnFocusReq = _tfcOnFocusReq t1 <> _tfcOnFocusReq t2,     _tfcOnBlurReq = _tfcOnBlurReq t1 <> _tfcOnBlurReq t2,     _tfcOnChangeReq = _tfcOnChangeReq t1 <> _tfcOnChangeReq t2@@ -204,7 +223,7 @@     _tfcCaretWidth = Just w   } -instance CmbCaretMs (TimeFieldCfg s e a) Int where+instance CmbCaretMs (TimeFieldCfg s e a) Millisecond where   caretMs ms = def {     _tfcCaretMs = Just ms   }@@ -229,6 +248,11 @@     _tfcSelectOnFocus = Just sel   } +instance CmbReadOnly (TimeFieldCfg s e a) where+  readOnly_ ro = def {+    _tfcReadOnly = Just ro+  }+ instance FormattableTime a => CmbMinValue (TimeFieldCfg s e a) a where   minValue len = def {     _tfcMinValue = Just len@@ -336,6 +360,7 @@   format = fromMaybe defaultTimeFormat (_tfcTimeFormat config)   minVal = _tfcMinValue config   maxVal = _tfcMaxValue config+  readOnly = fromMaybe False (_tfcReadOnly config)   initialValue     | isJust minVal = fromJust minVal     | isJust maxVal = fromJust maxVal@@ -364,9 +389,10 @@     _ifcDisplayChar = Nothing,     _ifcResizeOnChange = fromMaybe False (_tfcResizeOnChange config),     _ifcSelectOnFocus = fromMaybe True (_tfcSelectOnFocus config),+    _ifcReadOnly = readOnly,     _ifcStyle = Just L.timeFieldStyle,-    _ifcWheelHandler = Just (handleWheel config),-    _ifcDragHandler = Just (handleDrag config),+    _ifcWheelHandler = if readOnly then Nothing else Just (handleWheel config),+    _ifcDragHandler = if readOnly then Nothing else Just (handleDrag config),     _ifcDragCursor = Just CursorSizeV,     _ifcOnFocusReq = _tfcOnFocusReq config,     _ifcOnBlurReq = _tfcOnBlurReq config,@@ -439,7 +465,7 @@  timeFromTextSimple   :: (TimeOfDayConverter a, FormattableTime a)-  => TimeFormat+  => TimeFieldFormat   -> Text   -> Maybe a timeFromTextSimple format text = newTime where@@ -454,7 +480,7 @@       | otherwise -> makeTimeOfDayValid n1 n2 (fromIntegral n3)   newTime = tmpTime >>= timeFromTimeOfDay' -timeToTextSimple :: FormattableTime a => TimeFormat -> a -> Text+timeToTextSimple :: FormattableTime a => TimeFieldFormat -> a -> Text timeToTextSimple format val = result where   sep = T.singleton defaultTimeDelim   converted = timeToTimeOfDay' val@@ -470,7 +496,7 @@     | format == FormatHHMM = thh <> sep <> tmm     | otherwise = thh <> sep <> tmm <> sep <> tss -acceptTextInput :: TimeFormat -> Text -> Bool+acceptTextInput :: TimeFieldFormat -> Text -> Bool acceptTextInput format text = isRight (A.parseOnly parser text) where   numP = A.digit *> ""   delimP = A.char defaultTimeDelim *> ""
src/Monomer/Widgets/Singles/ToggleButton.hs view
@@ -6,15 +6,18 @@ Stability   : experimental Portability : non-portable -Toggle button widget, used for boolean properties.+Toggle button widget, used for boolean values. -Its behavior is equivalent to 'Monomer.Widgets.Singles.Checkbox' and-'Monomer.Widgets.Singles.LabeledCheckbox', with a different visual+@+toggleButton \"Toggle\" booleanLens+@++Its behavior is equivalent to "Monomer.Widgets.Singles.Checkbox" and+"Monomer.Widgets.Singles.LabeledCheckbox", with a different visual representation. -See 'Monomer.Widgets.Singles.OptionButton' for detailed notes.+See "Monomer.Widgets.Singles.OptionButton" for detailed notes. -}-{-# LANGUAGE BangPatterns #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE StrictData #-}@@ -59,8 +62,10 @@ - 'onFocusReq': 'WidgetRequest' to generate when focus is received. - 'onBlur': event to raise when focus is lost. - 'onBlurReq': 'WidgetRequest' to generate when focus is lost.-- 'onChange': event to raise when the value changes/is clicked.-- 'onChangeReq': 'WidgetRequest' to generate when the value changes/is clicked.+- 'onClick': event to raise when the value is clicked.+- 'onClickReq': 'WidgetRequest' to generate when the value is clicked.+- 'onChange': event to raise when the value changes.+- 'onChangeReq': 'WidgetRequest' to generate when the value changes. -} type ToggleButtonCfg = OptionButtonCfg 
src/Monomer/Widgets/Util/Widget.hs view
@@ -33,7 +33,6 @@   infoMatches,   nodeMatches,   handleWidgetIdChange,-  findWidgetIdFromPath,   delayedMessage,   delayedMessage_ ) where@@ -202,21 +201,15 @@         & L.requests %~ (SetWidgetPath widgetId newPath <|)     | otherwise = result --- | Returns the WidgetId associated to the given path, if any.-findWidgetIdFromPath :: WidgetEnv s e -> Path -> Maybe WidgetId-findWidgetIdFromPath wenv path = mwni ^? _Just . L.widgetId where-  branch = wenv ^. L.findBranchByPath $ path-  mwni = Seq.lookup (length branch - 1) branch- -- | Sends a message to the given node with a delay of n ms.-delayedMessage :: Typeable i => WidgetNode s e -> i -> Int -> WidgetRequest s e+delayedMessage :: Typeable i => WidgetNode s e -> i -> Millisecond -> WidgetRequest s e delayedMessage node msg delay = delayedMessage_ widgetId path msg delay where   widgetId = node ^. L.info . L.widgetId   path = node ^. L.info . L.path  -- | Sends a message to the given WidgetId with a delay of n ms. delayedMessage_-  :: Typeable i => WidgetId -> Path -> i -> Int -> WidgetRequest s e+  :: Typeable i => WidgetId -> Path -> i -> Millisecond -> WidgetRequest s e delayedMessage_ widgetId path msg delay = RunTask widgetId path $ do-  threadDelay (delay * 1000)+  threadDelay (fromIntegral delay * 1000)   return msg
test/unit/Monomer/Common/CursorIconSpec.hs view
@@ -21,7 +21,6 @@ import Data.Maybe import Data.Sequence (Seq(..)) import Data.Text (Text)-import Safe import Test.Hspec  import qualified Data.Map.Strict as M@@ -31,6 +30,7 @@ import Monomer.Core.Combinators import Monomer.Core.Themes.SampleThemes import Monomer.Event+import Monomer.Helper (headMay) import Monomer.Main import Monomer.TestUtil import Monomer.TestEventUtil
+ test/unit/Monomer/Core/StyleUtilSpec.hs view
@@ -0,0 +1,78 @@+{-|+Module      : Monomer.Core.StyleUtilSpec+Copyright   : (c) 2018 Francisco Vallarino+License     : BSD-3-Clause (see the LICENSE file)+Maintainer  : fjvallarino@gmail.com+Stability   : experimental+Portability : non-portable++Unit tests for StyleUtil functions.+-}+module Monomer.Core.StyleUtilSpec (spec) where++import Control.Lens+import Test.Hspec++import Monomer.Core.Combinators+import Monomer.Graphics (Color, rgb)+import Monomer.Widgets.Singles.Spacer++import qualified Monomer.Lens as L++red :: Color+red = rgb 255 0 0++spec :: Spec+spec = describe "StyleUtil" $ do+  styleStateSpec++styleStateSpec :: Spec+styleStateSpec = describe "StyleState actions" $ do+  describe "StyleState merge" $ do+    it "should merge basic styles" $ do+      checkStyleAction styleBasic L.basic mergedStyles++    it "should merge hover styles" $ do+      checkStyleAction styleHover L.hover mergedStyles++    it "should merge focus styles" $ do+      checkStyleAction styleFocus L.focus mergedStyles++    it "should merge hover focus styles" $ do+      checkStyleAction styleFocusHover L.focusHover mergedStyles++    it "should merge active styles" $ do+      checkStyleAction styleActive L.active mergedStyles++    it "should merge disabled styles" $ do+      checkStyleAction styleDisabled L.disabled mergedStyles++  describe "StyleState merge" $ do+    it "should set basic styles" $ do+      checkStyleAction styleBasicSet L.basic newStyles++    it "should set hover styles" $ do+      checkStyleAction styleHoverSet L.hover newStyles++    it "should set focus styles" $ do+      checkStyleAction styleFocusSet L.focus newStyles++    it "should set focus hover styles" $ do+      checkStyleAction styleFocusHoverSet L.focusHover newStyles++    it "should set active styles" $ do+      checkStyleAction styleActiveSet L.active newStyles++    it "should set disabled styles" $ do+      checkStyleAction styleDisabledSet L.disabled newStyles++  where+    baseStyles = [width 200, padding 10]+    newStyles = [padding 5, bgColor red]+    mergedStyles = [width 200, padding 5, bgColor red]++    checkStyleAction styleFn field targetStyles = do+      let node = styleFn spacer baseStyles+      let newNode = styleFn node newStyles++      newNode ^. L.info . L.style . field `shouldBe` Just (mconcat targetStyles)
test/unit/Monomer/TestUtil.hs view
@@ -56,27 +56,35 @@ testWindowRect :: Rect testWindowRect = Rect 0 0 testW testH -mockTextMetrics :: Font -> FontSize -> TextMetrics-mockTextMetrics font fontSize = TextMetrics {+mockTextMetrics :: Double -> Font -> FontSize -> TextMetrics+mockTextMetrics scale font fontSize = TextMetrics {   _txmAsc = 15,   _txmDesc = 5,   _txmLineH = 20,   _txmLowerX = 10 } -mockTextSize :: Maybe Double -> Font -> FontSize -> FontSpace -> Text -> Size-mockTextSize mw font (FontSize fs) spaceH text = Size width height where+mockTextSize+  :: Maybe Double -> Double -> Font -> FontSize -> FontSpace -> Text -> Size+mockTextSize mw scale font (FontSize fs) spaceH text = Size width height where   w = fromMaybe fs mw + unFontSpace spaceH   width = fromIntegral (T.length text) * w   height = 20  mockGlyphsPos-  :: Maybe Double -> Font -> FontSize -> FontSpace -> Text -> Seq GlyphPos-mockGlyphsPos mw font (FontSize fs) spaceH text = glyphs where+  :: Maybe Double+  -> Double+  -> Font+  -> FontSize+  -> FontSpace+  -> Text+  -> Seq GlyphPos+mockGlyphsPos mw scale font (FontSize fs) spaceH text = glyphs where   w = fromMaybe fs mw + unFontSpace spaceH   chars = Seq.fromList $ T.unpack text   mkGlyph idx chr = GlyphPos {     _glpGlyph = chr,+    _glpX = fromIntegral idx * w,     _glpXMin = fromIntegral idx * w,     _glpXMax = (fromIntegral idx + 1) * w,     _glpYMin = 0,@@ -154,15 +162,19 @@  mockFontManager :: FontManager mockFontManager = FontManager {-  computeTextMetrics = mockTextMetrics,-  computeTextSize = mockTextSize (Just 10),-  computeGlyphsPos = mockGlyphsPos (Just 10)+  computeTextMetrics = mockTextMetrics 1,+  computeTextMetrics_ = mockTextMetrics,+  computeTextSize = mockTextSize (Just 10) 1,+  computeTextSize_ = mockTextSize (Just 10),+  computeGlyphsPos = mockGlyphsPos (Just 10) 1,+  computeGlyphsPos_ = mockGlyphsPos (Just 10) }  mockWenv :: s -> WidgetEnv s e mockWenv model = WidgetEnv {   _weOs = "Mac OS X",   _weDpr = 2,+  _weAppStartTs = 0,   _weFontManager = mockFontManager,   _weFindBranchByPath = const Seq.empty,   _weMainButton = BtnLeft,
test/unit/Monomer/Widgets/CompositeSpec.hs view
@@ -17,9 +17,10 @@ module Monomer.Widgets.CompositeSpec (spec) where  import Control.Lens (-  (&), (^.), (^?), (^..), (.~), (%~), _Just, ix, folded, traverse, dropping)+  (&), (^.), (^?), (^?!), (^..), (.~), (%~), _Just, ix, folded, traverse, dropping) import Control.Lens.TH (abbreviatedFields, makeLensesWith) import Data.Default+import Data.Foldable (toList) import Data.Maybe import Data.Text (Text) import Data.Typeable (Typeable, cast)@@ -52,6 +53,10 @@   = MainBtnClicked   | ChildClicked   | MainResize Rect+  | OnInit+  | OnDispose+  | OnChange MainModel+  | ReportOnChange MainModel MainModel   deriving (Eq, Show)  data ChildEvt@@ -140,6 +145,9 @@ handleEvent :: Spec handleEvent = describe "handleEvent" $ do   handleEventBasic+  handleEventOnInit+  handleEventOnDispose+  handleEventOnChange   handleEventNewRoot   handleEventChild   handleEventResize@@ -171,6 +179,82 @@     model es = nodeHandleEventModel wenv es cmpNode     reqs es = nodeHandleEventReqs wenv es cmpNode +handleEventOnInit :: Spec+handleEventOnInit = describe "handleEventOnInit" $ do+  it "should generate an init event" $ do+    evts [] `shouldBe` Seq.singleton OnInit++  where+    wenv = mockWenv def+    handleEvent+      :: WidgetEnv MainModel MainEvt+      -> WidgetNode MainModel MainEvt+      -> MainModel+      -> MainEvt+      -> [EventResponse MainModel MainEvt MainModel MainEvt]+    handleEvent wenv node model evt = case evt of+      OnInit{} -> [Report evt]+      _ -> []+    buildUI wenv model = vstack []+    cmpNode = composite_ "main" id buildUI handleEvent [onInit OnInit]+    evts es = nodeHandleEventEvts_ wenv WInitKeepFirst es cmpNode++handleEventOnDispose :: Spec+handleEventOnDispose = describe "handleEventOnDispose" $ do+  it "should generate an init event" $ do+    let val = case evts [] ^?! L.requests . ix 1 of+          SendMessage wid msg -> cast msg+          _ -> Nothing++    val `shouldBe` Just OnDispose++  where+    wenv = mockWenv def+    handleEvent+      :: WidgetEnv MainModel MainEvt+      -> WidgetNode MainModel MainEvt+      -> MainModel+      -> MainEvt+      -> [EventResponse MainModel MainEvt MainModel MainEvt]+    handleEvent wenv node model evt = case evt of+      OnInit{} -> [Report evt]+      _ -> []+    buildUI wenv model = vstack []+    cmpNode = nodeInit wenv+      $ composite_ "main" id buildUI handleEvent [onDispose OnDispose]+    evts es = widgetDispose (cmpNode ^. L.widget) wenv cmpNode++handleEventOnChange :: Spec+handleEventOnChange = describe "handleEventOnChange" $ do+  it "should not generate an event if model did not change" $ do+    evts [evtClick (Point 10 30)] `shouldBe` Seq.empty++  it "should generate an event if model changed" $ do+    let items = toList $ evts [evtClick (Point 10 10)]+    let ReportOnChange oldModel newModel = head items++    oldModel `shouldNotBe` newModel++  where+    wenv = mockWenv def+    handleEvent+      :: WidgetEnv MainModel MainEvt+      -> WidgetNode MainModel MainEvt+      -> MainModel+      -> MainEvt+      -> [EventResponse MainModel MainEvt MainModel MainEvt]+    handleEvent wenv node model evt = case evt of+      MainBtnClicked -> [Model (model & clicks %~ (+1))]+      ChildClicked -> [Model model]+      OnChange oldModel -> [Report $ ReportOnChange oldModel model]+      _ -> []+    buildUI wenv model = vstack [+        button "Click main" MainBtnClicked,+        button "Click secondary" ChildClicked+      ]+    cmpNode = composite_ "main" id buildUI handleEvent [onChange OnChange]+    evts es = nodeHandleEventEvts wenv es cmpNode+ handleEventNewRoot :: Spec handleEventNewRoot = describe "handleEventNewRoot" $ do   it "should generate a resize request when the widgetType of the root widget changes" $ do@@ -340,7 +424,7 @@         ] `nodeKey` "localTxt1"       ]     cmpNode1 = composite "main" id buildUI1 handleEvent-    cmpNode2 = composite_ "main" id buildUI2 handleEvent [mergeRequired (\_ _ -> True)]+    cmpNode2 = composite_ "main" id buildUI2 handleEvent [mergeRequired (\_ _ _ -> True)]     evts1 = [evtK keyTab, evtT "aacc", moveCharL, moveCharL]     (wenv1, root1, _) = fst $ nodeHandleEvents wenv WInit evts1 cmpNode1     cntNodeM = nodeMerge wenv1 cmpNode2 root1@@ -473,7 +557,7 @@     cmpNode = findByHelperUI     wni path = res where       inode = nodeInit wenv cmpNode-      res = findWidgetByPath wenv inode path+      res = findChildNodeInfoByPath wenv inode path  findNextFocus :: Spec findNextFocus = describe "findNextFocus" $ do
test/unit/Monomer/Widgets/Containers/BoxSpec.hs view
@@ -68,8 +68,8 @@     btnNew = button "Click" (BtnClick 0) `nodeKey` "btnNew"     btnOld = button "Click" (BtnClick 0) `nodeKey` "btnOld"     box1 = box btnNew-    box2 = box_ [mergeRequired (\_ _ -> True)] btnNew-    box3 = box_ [mergeRequired (\_ _ -> False)] btnNew+    box2 = box_ [mergeRequired (\_ _ _ -> True)] btnNew+    box3 = box_ [mergeRequired (\_ _ _ -> False)] btnNew     boxM = box btnOld     mergeWith newNode oldNode = result ^?! L.node . L.children . ix 0 where       oldNode2 = nodeInit wenv oldNode
test/unit/Monomer/Widgets/Containers/KeystrokeSpec.hs view
@@ -23,25 +23,30 @@  import qualified Data.Sequence as Seq -import Monomer.Core+import Monomer.Common import Monomer.Core.Combinators import Monomer.Event import Monomer.TestUtil import Monomer.TestEventUtil import Monomer.Widgets.Containers.Keystroke+import Monomer.Widgets.Singles.Button import Monomer.Widgets.Singles.Label import Monomer.Widgets.Singles.TextField  import qualified Monomer.Lens as L  data TestEvt-  = SingleO+  = ButtonEvent+  | EnterPressed+  | SingleO   | TextFieldChanged Text   | CtrlA   | CtrlSpace+  | CtrlDash   | CtrlShiftSpace   | MultiKey Int   | FunctionKey Int+  | SymbolKey Text   deriving (Eq, Show)  newtype TestModel = TestModel {@@ -53,6 +58,7 @@ spec :: Spec spec = describe "Keystroke" $ do   handleEvent+  handleNestedEvent   getSizeReq  handleEvent :: Spec@@ -63,6 +69,13 @@   it "should generate an event when Ctrl-Space is pressed" $ do     events [evtKC keySpace] `shouldBe` Seq.fromList [CtrlSpace] +  it "should generate an event when Ctrl-Dash is pressed" $ do+    let wenv = mockWenv (TestModel "")+          & L.inputStatus . L.keyMod . L.leftCtrl .~ True+    let events es = nodeHandleEventEvts wenv es kstNode++    events [evtT "-"] `shouldBe` Seq.fromList [CtrlDash]+   it "should generate an event when Ctrl-Shift-Space is pressed" $ do     events [evtKCS keySpace] `shouldBe` Seq.fromList [CtrlShiftSpace] @@ -72,6 +85,14 @@     events [evtKG keyF7] `shouldBe` Seq.fromList [FunctionKey 7]     events [evtKS keyF12] `shouldBe` Seq.fromList [FunctionKey 12] +  it "should generate events when symbol keys are pressed" $ do+    let wenv = mockWenv (TestModel "")+          & L.inputStatus . L.keyMod . L.leftCtrl .~ True+    let events es = nodeHandleEventEvts wenv es kstNode++    events [evtT "["] `shouldBe` Seq.fromList [SymbolKey "["]+    events [evtT "^"] `shouldBe` Seq.fromList [SymbolKey "^"]+   it "should only generate events when the exact keys are pressed" $ do     events [evtKC keyA, evtKC keyB] `shouldBe` Seq.fromList []     events [evtKC keyA, evtKC keyB, evtKC keyD, evtKC keyC] `shouldBe` Seq.fromList []@@ -104,13 +125,16 @@     wenv = mockWenv (TestModel "")     bindings = [         ("C-Space", CtrlSpace),+        ("C-Dash", CtrlDash),         ("C-S-Space", CtrlShiftSpace),         ("C-a-b-c", MultiKey 1),         ("C-d-e", MultiKey 2),         ("F1", FunctionKey 1),         ("Ctrl-F3", FunctionKey 3),         ("Cmd-F7", FunctionKey 7),-        ("S-F12", FunctionKey 12)+        ("S-F12", FunctionKey 12),+        ("C-[", SymbolKey "["),+        ("Shift-^", SymbolKey "^")       ]     kstNode = keystroke bindings (textField textValue)     events es = nodeHandleEventEvts wenv es kstNode@@ -121,6 +145,35 @@     model2 es = nodeHandleEventModel wenv2 es kstModel2     events1 es = nodeHandleEventEvts wenv2 es kstModel1     events2 es = nodeHandleEventEvts wenv2 es kstModel2++handleNestedEvent :: Spec+handleNestedEvent = describe "handleNestedEvent" $ do+  it "should not generate events" $ do+    events False False [] `shouldBe` Seq.empty++  it "should generate an event when clicking the button" $ do+    events False False [evtClick (Point 100 100)] `shouldBe` Seq.fromList [ButtonEvent]+    events True False [evtClick (Point 100 100)] `shouldBe` Seq.fromList [ButtonEvent]+    events False True [evtClick (Point 100 100)] `shouldBe` Seq.fromList [ButtonEvent]+    events True True [evtClick (Point 100 100)] `shouldBe` Seq.fromList [ButtonEvent]++  it "should generate the expected events when pressing the enter key" $ do+    events False False [evtK keyEnter] `shouldBe` Seq.fromList [EnterPressed, ButtonEvent]+    events True False [evtK keyEnter] `shouldBe` Seq.fromList [EnterPressed]+    events False True [evtK keyEnter] `shouldBe` Seq.fromList [ButtonEvent]+    events True True [evtK keyEnter] `shouldBe` Seq.fromList [EnterPressed]++  where+    wenv = mockWenv (TestModel "")+    bindings = [+        ("Enter", EnterPressed)+      ]+    kstNode ignoreChild ignoreParent =+      keystroke_ bindings [ignoreChildrenEvts_ ignoreChild] $+        button_ "Test" ButtonEvent [ignoreParentEvts_ ignoreParent]+    events ignoreChild ignoreParent es =+      nodeHandleEventEvts wenv es $+        kstNode ignoreChild ignoreParent  getSizeReq :: Spec getSizeReq = describe "getSizeReq" $ do
test/unit/Monomer/Widgets/Containers/ScrollSpec.hs view
@@ -51,6 +51,7 @@ handleEvent :: Spec handleEvent = describe "handleEvent" $ do   handleBarClick+  handleThumbDrag   handleChildrenFocus   handleNestedWheel   handleMessageReset@@ -61,15 +62,12 @@   it "should click the first button" $ do     evts [evtClick point] `shouldBe` Seq.fromList [Button1] -  it "should scroll right and click the third button" $ do+  it "should scroll right and click the second button" $ do     evts [evtPress midHBar, evtClick point] `shouldBe` Seq.fromList [Button2]    it "should scroll down and click the third button" $ do     evts [evtPress midVBar, evtClick point] `shouldBe` Seq.fromList [Button3] -  it "should scroll down and click the third button" $ do-    evts [evtPress midVBar, evtClick point] `shouldBe` Seq.fromList [Button3]-   it "should scroll down and right and click the fourth button" $ do     evts [evtPress midHBar, evtPress midVBar, evtClick point] `shouldBe` Seq.fromList [Button4] @@ -93,6 +91,53 @@       ]     scrollNode = scroll stackNode     evts es = nodeHandleEventEvts wenv es scrollNode++handleThumbDrag :: Spec+handleThumbDrag = describe "handleThumbDrag" $ do+  it "should click the first button" $ do+    evts [evtClick pClick] `shouldBe` Seq.fromList [Button1]++  it "should drag the thumb right and click the second button" $ do+    evts [evtPress pStartH, evtMove pEndH, evtClick pClick] `shouldBe` Seq.fromList [Button2]++  it "should drag the thumb down and click the third button" $ do+    evts [evtPress pStartV, evtMove pEndV, evtClick pClick] `shouldBe` Seq.fromList [Button3]++  it "should drag the thumb down and right and click the fourth button" $ do+    let steps = [ evtPress pStartH, evtMove pEndH, evtRelease pEndH,+                  evtPress pStartV, evtMove pEndV, evtRelease pEndV,+                  evtClick pClick ]+    evts steps `shouldBe` Seq.fromList [Button4]++  it "should fail to drag the thumb right because of thumb size, causing it to click the first button" $ do+    evtsSmall [evtPress pStartH, evtMove pEndH, evtClick pClick] `shouldBe` Seq.fromList [Button1]++  it "should fail to drag the thumb down because of thumb size, causing it to click the first button" $ do+    evtsSmall [evtPress pStartV, evtMove pEndV, evtClick pClick] `shouldBe` Seq.fromList [Button1]++  where+    wenv = mockWenv ()+      & L.theme .~ darkTheme+      & L.windowSize .~ Size 640 480+    pClick = Point 320 200+    pStartH = Point 50 476+    pEndH = Point 400 476+    pStartV = Point 636 50+    pEndV = Point 636 300+    st = [width 6400, height 4800]+    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_ [thumbMinSize 100] stackNode+    evts es = nodeHandleEventEvts wenv es scrollNode+    evtsSmall es = nodeHandleEventEvts wenv es (scroll stackNode)  handleChildrenFocus :: Spec handleChildrenFocus = describe "handleChildrenFocus" $ do
test/unit/Monomer/Widgets/Singles/DateFieldSpec.hs view
@@ -72,6 +72,7 @@   handleEventDate   handleEventValueDate   handleEventMouseDragDate+  handleEventReadOnly   handleShiftFocus   getSizeReqDate   testWidgetType@@ -246,6 +247,30 @@     model es = nodeHandleEventModel wenv es dateNode     lastIdx es = Seq.index es (Seq.length es - 1)     lastEvt es = lastIdx (evts es)++handleEventReadOnly :: Spec+handleEventReadOnly = describe "handleEventReadOnly" $ do+  it "should ignore text input" $ do+    let steps = [moveCharR, delCharL, evtT "5"]+    model steps ^. dateValue `shouldBe` initDate+  +  it "should ignore drag" $ do+    let selStart = Point 5 5+    let selEnd = Point 100 2000+    let steps = [evtPress selStart, evtMove selEnd, evtRelease selEnd]+    model steps ^. dateValue `shouldBe` initDate++  it "should ignore wheel" $ do+    let steps = [WheelScroll (Point 100 10) (Point 0 (-2000)) WheelNormal]+    model steps ^. dateValue `shouldBe` initDate++  where+    initDate = fromGregorian 1999 11 21+    wenv = mockWenv (DateModel initDate True)+      & L.inputStatus . L.keyMod . L.leftShift .~ True+    dateCfg = [readOnly :: DateFieldCfg DateModel TestEvt Day]+    dateNode = dateField_ dateValue dateCfg+    model es = nodeHandleEventModel wenv es dateNode  handleShiftFocus :: Spec handleShiftFocus = describe "handleShiftFocus" $ do
test/unit/Monomer/Widgets/Singles/LabelSpec.hs view
@@ -121,7 +121,7 @@    where     fontMgr = mockFontManager {-      computeGlyphsPos = mockGlyphsPos Nothing+      computeGlyphsPos = mockGlyphsPos Nothing 1     }     wenv = mockWenvEvtUnit ()       & L.fontManager .~ fontMgr
test/unit/Monomer/Widgets/Singles/NumericFieldSpec.hs view
@@ -65,6 +65,7 @@   handleEventIntegral   handleEventValueIntegral   handleEventMouseDragIntegral+  handleEventReadOnly   getSizeReqIntegral   testIntegralWidgetType @@ -475,6 +476,30 @@         numericField_ fractionalValue [wheelRate 1, onFocus GotFocus]       ]     evts es = nodeHandleEventEvts wenv es floatNode++handleEventReadOnly :: Spec+handleEventReadOnly = describe "handleEventReadOnly" $ do+  it "should ignore text input" $ do+    let steps = [moveCharR, delCharL, evtT "5"]+    model steps ^. fractionalValue `shouldBe` initValue+  +  it "should ignore drag" $ do+    let selStart = Point 5 5+    let selEnd = Point 100 2000+    let steps = [evtPress selStart, evtMove selEnd, evtRelease selEnd]+    model steps ^. fractionalValue `shouldBe` initValue++  it "should ignore wheel" $ do+    let steps = [WheelScroll (Point 100 10) (Point 0 (-2000)) WheelNormal]+    model steps ^. fractionalValue `shouldBe` initValue++  where+    initValue = Just 42.4+    wenv = mockWenv (FractionalModel initValue False)+      & L.inputStatus . L.keyMod . L.leftShift .~ True+    fieldCfg = [readOnly :: NumericFieldCfg FractionalModel TestEvt (Maybe Double)]+    fieldNode = numericField_ fractionalValue fieldCfg+    model es = nodeHandleEventModel wenv es fieldNode  getSizeReqFractional :: Spec getSizeReqFractional = describe "getSizeReqFractional" $ do
test/unit/Monomer/Widgets/Singles/OptionButtonSpec.hs view
@@ -42,7 +42,8 @@   deriving (Eq, Show)  data FruitEvt-  = FruitSel Fruit+  = FruitClicked+  | FruitSel Fruit   | GotFocus Path   | LostFocus Path   deriving (Eq, Show)@@ -77,11 +78,21 @@   it "should generate an event when focus is lost" $     events evtBlur orangeNode `shouldBe` Seq.singleton (LostFocus emptyPath) +  it "should generate multiple click events when clicked, but a single onChange because the value did not change" $ do+    let evt = evtClick (Point 320 240)+    let events es = nodeHandleEventEvts wenv es bananaClickNode++    events [evt] `shouldBe` Seq.fromList [FruitClicked, FruitSel Banana]+    events [evt, evt] `shouldBe` Seq.fromList [FruitClicked, FruitSel Banana, FruitClicked]+    events [evt, evt, evt] `shouldBe` Seq.fromList [FruitClicked, FruitSel Banana, FruitClicked, FruitClicked]+   where     wenv = mockWenv (TestModel Apple)     orangeNode = optionButton_ "Orange" Orange fruit [onFocus GotFocus, onBlur LostFocus]     bananaNode :: WidgetNode TestModel FruitEvt     bananaNode = optionButton "Banana" Banana fruit+    bananaClickNode = optionButton_ "Banana" Banana fruit [onClick FruitClicked, onChange FruitSel]+     clickModel p node = nodeHandleEventModel wenv [evtClick p] node     keyModel key node = nodeHandleEventModel wenv [KeyAction def key KeyPressed] node     events evt node = nodeHandleEventEvts wenv [evt] node
test/unit/Monomer/Widgets/Singles/RadioSpec.hs view
@@ -40,7 +40,8 @@   deriving (Eq, Show)  data FruitEvt-  = FruitSel Fruit+  = FruitClicked+  | FruitSel Fruit   | GotFocus Path   | LostFocus Path   deriving (Eq, Show)@@ -75,12 +76,22 @@   it "should generate an event when focus is lost" $     events evtBlur orangeNode `shouldBe` Seq.singleton (LostFocus emptyPath) +  it "should generate multiple click events when clicked, but a single onChange because the value did not change" $ do+    let evt = evtClick (Point 320 240)+    let events es = nodeHandleEventEvts wenv es bananaClickNode++    events [evt] `shouldBe` Seq.fromList [FruitClicked, FruitSel Banana]+    events [evt, evt] `shouldBe` Seq.fromList [FruitClicked, FruitSel Banana, FruitClicked]+    events [evt, evt, evt] `shouldBe` Seq.fromList [FruitClicked, FruitSel Banana, FruitClicked, FruitClicked]+   where     wenv = mockWenv (TestModel Apple)       & L.theme .~ darkTheme     orangeNode = radio_ Orange fruit [onFocus GotFocus, onBlur LostFocus]     bananaNode :: WidgetNode TestModel FruitEvt     bananaNode = radio Banana fruit+    bananaClickNode = radio_ Banana fruit [onClick FruitClicked, onChange FruitSel]+     clickModel p node = nodeHandleEventModel wenv [evtClick p] node     keyModel key node = nodeHandleEventModel wenv [KeyAction def key KeyPressed] node     events evt node = nodeHandleEventEvts wenv [evt] node
test/unit/Monomer/Widgets/Singles/TextAreaSpec.hs view
@@ -51,6 +51,7 @@   handleEventValue   handleEventMouseSelect   handleEventHistory+  handleEventReadOnly   getSizeReq  handleEvent :: Spec@@ -322,6 +323,24 @@     evts es = nodeHandleEventEvts wenv es txtNode     lastIdx es = Seq.index es (Seq.length es - 1)     lastEvt es = lastIdx (evts es)++handleEventReadOnly :: Spec+handleEventReadOnly = describe "handleEventReadOnly" $ do+  it "should ignore text input" $ do+    model [evtT "a"] ^. textValue `shouldBe` initText+  +  it "should ignore cut" $ do+    model [selWordR, evtKG keyX] ^. textValue `shouldBe` initText+  +  it "should ignore paste" $ do+    model [selWordR, evtKG keyV] ^. textValue `shouldBe` initText+  +  where+    initText = "hello"+    wenv = mockWenv (TestModel initText)+    txtCfg = [readOnly :: TextAreaCfg TestModel TestEvt]+    txtNode = textArea_ textValue txtCfg+    model es = nodeHandleEventModel wenv es txtNode  getSizeReq :: Spec getSizeReq = describe "getSizeReq" $ do
test/unit/Monomer/Widgets/Singles/TextFieldSpec.hs view
@@ -51,6 +51,9 @@   handleEventValue   handleEventMouseSelect   handleEventHistory+  handleEventMouseDrag+  handleEventWheel+  handleEventReadOnly   getSizeReq  handleEvent :: Spec@@ -267,6 +270,48 @@     evts es = nodeHandleEventEvts wenv es txtNode     lastIdx es = Seq.index es (Seq.length es - 1)     lastEvt es = lastIdx (evts es)++handleEventMouseDrag :: Spec+handleEventMouseDrag = describe "handleEventMouseDrag" $ do+  it "should ignore shift+drag events" $ do+    let selStart = Point 50 10+    let selEnd = Point 50 (-70)+    let steps = [evtPress selStart, evtMove selEnd, evtRelease selEnd]+    model steps ^. textValue `shouldBe` ""++  where+    wenv = mockWenvEvtUnit (TestModel "")+      & L.inputStatus . L.keyMod . L.leftShift .~ True+    model es = nodeHandleEventModel wenv es (textField textValue)++handleEventWheel :: Spec+handleEventWheel = describe "handleEventWheel" $ do+  it "should ignore wheel events" $ do+    let p = Point 50 10+    let steps1 = [WheelScroll p (Point 0 (-8000)) WheelNormal]+    model steps1 ^. textValue `shouldBe` ""++  where+    wenv = mockWenvEvtUnit (TestModel "")+    model es = nodeHandleEventModel wenv es (textField textValue)++handleEventReadOnly :: Spec+handleEventReadOnly = describe "handleEventReadOnly" $ do+  it "should ignore text input" $ do+    model [evtT "a"] ^. textValue `shouldBe` initText+  +  it "should ignore cut" $ do+    model [selWordR, evtKG keyX] ^. textValue `shouldBe` initText+  +  it "should ignore paste" $ do+    model [selWordR, evtKG keyV] ^. textValue `shouldBe` initText+  +  where+    initText = "hello"+    wenv = mockWenv (TestModel initText)+    txtCfg = [readOnly :: TextFieldCfg TestModel TestEvt]+    txtNode = textField_ textValue txtCfg+    model es = nodeHandleEventModel wenv es txtNode  getSizeReq :: Spec getSizeReq = describe "getSizeReq" $ do
test/unit/Monomer/Widgets/Singles/TimeFieldSpec.hs view
@@ -72,6 +72,7 @@   handleEventTime   handleEventValueTime   handleEventMouseDragTime+  handleEventReadOnly   handleShiftFocus   getSizeReqTime   testWidgetType@@ -255,6 +256,30 @@     model es = nodeHandleEventModel wenv es timeNode     lastIdx es = Seq.index es (Seq.length es - 1)     lastEvt es = lastIdx (evts es)++handleEventReadOnly :: Spec+handleEventReadOnly = describe "handleEventReadOnly" $ do+  it "should ignore text input" $ do+    let steps = [moveCharR, delCharL, evtT "5"]+    model steps ^. timeValue `shouldBe` initTime+  +  it "should ignore drag" $ do+    let selStart = Point 5 5+    let selEnd = Point 100 2000+    let steps = [evtPress selStart, evtMove selEnd, evtRelease selEnd]+    model steps ^. timeValue `shouldBe` initTime++  it "should ignore wheel" $ do+    let steps = [WheelScroll (Point 100 10) (Point 0 (-2000)) WheelNormal]+    model steps ^. timeValue `shouldBe` initTime+  +  where+    initTime = TimeOfDay 20 37 00+    wenv = mockWenv (TimeModel initTime False)+      & L.inputStatus . L.keyMod . L.leftShift .~ True+    timeCfg = [readOnly :: TimeFieldCfg TimeModel TestEvt TimeOfDay]+    timeNode = timeField_ timeValue timeCfg+    model es = nodeHandleEventModel wenv es timeNode  handleShiftFocus :: Spec handleShiftFocus = describe "handleShiftFocus" $ do
test/unit/Monomer/Widgets/Singles/ToggleButtonSpec.hs view
@@ -36,7 +36,8 @@ import qualified Monomer.Lens as L  data TestEvt-  = BoolSel Bool+  = BoolClicked+  | BoolSel Bool   | GotFocus Path   | LostFocus Path   deriving (Eq, Show)@@ -70,9 +71,19 @@   it "should generate an event when focus is lost" $     events evtBlur `shouldBe` Seq.singleton (LostFocus emptyPath) +  it "should generate multiple click and change events when clicked, since it toggles between True and False" $ do+    let evt = evtClick (Point 320 240)+    let events es = nodeHandleEventEvts wenv es chkClickNode++    events [evt] `shouldBe` Seq.fromList [BoolClicked, BoolSel True]+    events [evt, evt] `shouldBe` Seq.fromList [BoolClicked, BoolSel True, BoolClicked, BoolSel False]+    events [evt, evt, evt] `shouldBe` Seq.fromList [BoolClicked, BoolSel True, BoolClicked, BoolSel False, BoolClicked, BoolSel True]+   where     wenv = mockWenv (TestModel False)     chkNode = toggleButton_ "Toggle" testBool [onFocus GotFocus, onBlur LostFocus]+    chkClickNode = toggleButton_ "Toggle" testBool [onClick BoolClicked, onChange BoolSel]+     clickModel p = nodeHandleEventModel wenv [evtClick p] chkNode     keyModel key = nodeHandleEventModel wenv [KeyAction def key KeyPressed] chkNode     events evt = nodeHandleEventEvts wenv [evt] chkNode
test/unit/Monomer/Widgets/Util/TextSpec.hs view
@@ -152,6 +152,16 @@     clipKeep "This is    a tad bit longer\nMore" ^. ix 1 . L.text `shouldBe` "   a tad"     clipKeep "This is    a tad bit longer\nMore" ^. ix 2 . L.text `shouldBe` " bit " +  it "should return text broken even in the middle of words, clipped, trimmed, if it does not fit" $ do+    breakOnChar "This is    really-long\nMore" `shouldSatisfy` elementCount 3+    breakOnChar "This is    really-long\nMore" ^. ix 0 . L.text `shouldBe` "This is"+    breakOnChar "This is    really-long\nMore" ^. ix 1 . L.text `shouldBe` "reall"+    breakOnChar "This is    really-long\nMore" ^. ix 2 . L.text `shouldBe` "y-long"+    breakOnChar "This is    a tad bit longer\nMore" `shouldSatisfy` elementCount 3+    breakOnChar "This is    a tad bit longer\nMore" ^. ix 0 . L.text `shouldBe` "This is"+    breakOnChar "This is    a tad bit longer\nMore" ^. ix 1 . L.text `shouldBe` "a tad"+    breakOnChar "This is    a tad bit longer\nMore" ^. ix 2 . L.text `shouldBe` "bit lon"+   where     wenv = mockWenv ()     fontMgr = wenv ^. L.fontManager@@ -167,6 +177,7 @@     elpsKeep_ size text = fitTextToSize fontMgr style Ellipsis MultiLine KeepSpaces Nothing size text     clipTrim_ size text = fitTextToSize fontMgr style ClipText MultiLine TrimSpaces Nothing size text     clipKeep_ size text = fitTextToSize fontMgr style ClipText MultiLine KeepSpaces Nothing size text+    breakOnChar text = fitTextToSize fontMgr (textLineBreak OnCharacters) ClipText MultiLine TrimSpaces Nothing sizeC text     elementCount count sq = Seq.length sq == count  fitTextSpace :: Spec
test/unit/Spec.hs view
@@ -8,7 +8,10 @@ import Monomer.TestUtil (useVideoSubSystem)  import qualified Monomer.Common.CursorIconSpec as CursorIconSpec+ import qualified Monomer.Core.SizeReqSpec as SizeReqSpec+import qualified Monomer.Core.StyleUtilSpec as StyleUtilSpec+ import qualified Monomer.Graphics.UtilSpec as GraphicsUtilSpec  import qualified Monomer.Widgets.CompositeSpec as CompositeSpec@@ -88,6 +91,7 @@ core :: Spec core = describe "Core" $ do   SizeReqSpec.spec+  StyleUtilSpec.spec  graphics :: Spec graphics = describe "Graphics" $ do