Initial version of themes tutorial. Initial version of light theme

This commit is contained in:
Francisco Vallarino 2021-07-20 01:59:54 -03:00
parent b5c8264370
commit 0038ff20ab
8 changed files with 341 additions and 116 deletions

2
.ghcid
View File

@ -1,3 +1,3 @@
--command "stack repl --main-is monomer:exe:monomer-exe"
--command "stack repl --main-is monomer:exe:tutorial"
--test ":main"
--restart=package.yaml

View File

@ -1,11 +1,11 @@
# Custom Widgets
Sometimes you can't build the UI you need using only the available widgets. In
that case, you can create a custom widget.
Sometimes you can't build the UI you need using only the stock widgets. In that
case, you can create a custom widget.
Custom widgets give you the ability to handle mouse and keyboard events, render
arbitrary content to the screen, access to the clipboard and more explicit
access to other feature which we have been using, such as tasks and producers.
Custom widgets give you the ability to handle mouse and keyboard events, to
render arbitrary content to the screen and provide more explicit control over
other features we have been using, such as tasks and producers.
Monomer provides the `Widget` type, which represents the interface for creating
widgets. As an interface, it requires implementing everything from scratch,
@ -21,7 +21,7 @@ using either of:
## Common lifecycle functions
Although their prefix will be different (single vs container), for both type of
Although their prefix will be different (single vs container), for both types of
widgets you may be interested in overriding these functions:
- Init: Called the first time a widget is added to the widget tree. It's useful
@ -42,7 +42,7 @@ widgets you may be interested in overriding these functions:
Renderer interface provides low level rendering functions, while the drawing
module provides some convenience higher level tools.
Both Single and Container have other overridable functions and properties. Check
Both Single and Container have other overridable functions and attributes. Check
their respective Haddocks for more details.
## Single
@ -93,15 +93,15 @@ merge wenv node oldNode oldState = result where
You may wonder why the oldNode is not used directly, or at least its widget. The
reason for that is that, for the node, the styling may have changed when the new
UI is built. For the widget, since we reference the config parameter (which may
UI was built. For the widget, since we reference the config parameter (which may
also have changed), if we keep the old widget we'd still be using the previous
version of the config.
### WidgetResult
Some operations return a WidgetResponse, which contains the new version of the
node plus a list of WidgetRequests. There are few helpers you will see across
the library which help creating instances of this type:
node plus a list of WidgetRequests. There are few helpers available for creating
instances of this type:
- resultNode: updates the node, without requests.
- resultReqs: updates the node and includes requests.
@ -114,9 +114,9 @@ you need to have control regarding the order in which your requests happen.
### Handle event
The standard way to implement handleEvent is to pattern match on the `evt` argument,
handle the events of interest and return Nothing for the rest. In the example
`Click` and `Move` are handled.
The standard way to implement handleEvent is to pattern match on the `evt`
argument, handle the events of interest and return Nothing for the rest. In the
example `Click` and `Move` are handled.
```haskell
handleEvent wenv node target evt = case evt of
@ -131,6 +131,10 @@ means, if rendering is not requested, the new line will not be displayed until
clicking the button again. In case you need to render periodically, you may want
to check `RenderEvery`.
Both Single and Container provide support for handling style changes based on
status (hover, active, etc); in those cases, the `RenderOnce` request is created
automatically.
### Handle message
The case of `handleMessage` is similar but, since the `msg` argument is an
@ -145,9 +149,7 @@ generated by them.
...
```
Typeable is used to allow widgets to provide their own message type, which would
otherwise become hard to manage if it had to be declared explicitly in the
widget tree type.
Typeable is used to allow widgets to provide their own message type.
### Size requirement

View File

@ -0,0 +1,35 @@
# Themes
Themes provide a way of customizing the base style of widgets across the entire
application. While in general you can customize the style of widgets on a per
instance basis, and you can create regular functions that return widgets styled
the way you need, themes provide a couple of useful features:
- They provide a quick way of switching the active theme on a given section of
the application. This is especially useful for changing colors.
- They allow customizing widgets that you may not have direct control over, such
as the scrollbar of a textArea or the background color of a dialog.
## Switching themes
The `themeSwitcher` widget, which receives a theme instance, optional config and
a single child widget, allows replacing the main application theme with a new
one for a specific section.
As shown in the accompanying application, one use case for themes is to allow
changing the style of an application at runtime. Since the example uses the
switcher at the top level, and the background color plays an important role, we
enable the corresponding config flag (disabled by default).
```haskell
themeSwitch_ theme [themeClearBg] childNode
```
Another use case for theme switching is when we want to customize composite
widgets, such as dialogs, which do not expose style configuration options. In
those cases, theme switching allows us to alter the default styles of child
widgets such as buttons, scrolls, etc.
## BaseTheme
## General Theme type

View File

@ -7,13 +7,15 @@ import qualified Tutorial04_Tasks
import qualified Tutorial05_Producers
import qualified Tutorial06_Composite
import qualified Tutorial07_CustomWidget
import qualified Tutorial08_Themes
main :: IO ()
main = do
-- Tutorial01_Basics.main01
-- Tutorial02_Styling.main02
Tutorial03_LifeCycle.main03
-- Tutorial03_LifeCycle.main03
-- Tutorial04_Tasks.main04
-- Tutorial05_Producers.main05
-- Tutorial06_Composite.main06
-- Tutorial07_CustomWidget.main07
Tutorial08_Themes.main08

View File

@ -0,0 +1,91 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
module Tutorial08_Themes where
import Control.Lens
import Data.Text (Text)
import Monomer
import Monomer.Core.Themes.BaseTheme
import TextShow
import qualified Monomer.Lens as L
data ActiveTheme
= DarkTheme
| LightTheme
| CustomTheme
deriving (Eq, Enum, Show)
data AppModel = AppModel {
_clickCount :: Int,
_currentTheme :: ActiveTheme
} deriving (Eq, Show)
data AppEvent
= AppInit
| AppIncrease
deriving (Eq, Show)
makeLenses 'AppModel
buildUI
:: WidgetEnv AppModel AppEvent
-> AppModel
-> WidgetNode AppModel AppEvent
buildUI wenv model = widgetTree where
theme = case model ^. currentTheme of
DarkTheme -> darkTheme
LightTheme -> lightTheme
CustomTheme -> customTheme
widgetTree = themeSwitch_ theme [themeClearBg] $ vstack [
hstack [
label "Select theme:",
spacer,
textDropdownS currentTheme (enumFrom (toEnum 0))
],
spacer,
separatorLine,
spacer,
label "Number",
spacer,
hstack [
box $ hslider clickCount 0 100,
spacer,
numericField_ clickCount [minValue 0, maxValue 100]
],
spacer,
hstack [
label $ "Click count: " <> showt (model ^. clickCount),
spacer,
button "Increase count" AppIncrease,
spacer,
mainButton "Increase count" AppIncrease
]
] `style` [padding 20]
handleEvent
:: WidgetEnv AppModel AppEvent
-> WidgetNode AppModel AppEvent
-> AppModel
-> AppEvent
-> [AppEventResponse AppModel AppEvent]
handleEvent wenv node model evt = case evt of
AppInit -> []
AppIncrease -> [Model (model & clickCount +~ 1)]
customTheme = baseTheme darkThemeColors {
btnMainBgBasic = orange
}
main08 :: IO ()
main08 = do
startApp model handleEvent buildUI config
where
config = [
appWindowTitle "Tutorial 08 - Themes",
appTheme darkTheme,
appFontDef "Regular" "./assets/fonts/Roboto-Regular.ttf",
appInitEvent AppInit
]
model = AppModel 0 LightTheme

View File

@ -409,6 +409,7 @@ executable tutorial
Tutorial05_Producers
Tutorial06_Composite
Tutorial07_CustomWidget
Tutorial08_Themes
Paths_monomer
hs-source-dirs:
examples/tutorial

View File

@ -8,7 +8,12 @@ Portability : non-portable
Provides sample color schemes for the base theme.
-}
module Monomer.Core.Themes.SampleThemes where
module Monomer.Core.Themes.SampleThemes (
lightTheme,
lightThemeColors,
darkTheme,
darkThemeColors
) where
import Control.Lens ((&), (^.), (.~), (?~), non)
@ -18,99 +23,183 @@ import Monomer.Graphics
import qualified Monomer.Lens as L
lightTheme :: Theme
lightTheme = baseTheme lightThemeColors
lightThemeColors :: BaseThemeColors
lightThemeColors = BaseThemeColors {
clearColor = grayBgLight,
btnFocusBorder = blueHighlightBtn,
btnBgBasic = grayLighter,
btnBgHover = grayBright,
btnBgActive = grayLight,
btnBgDisabled = grayDisabled,
btnText = grayDarker,
btnTextDisabled = grayDarker,
btnMainFocusBorder = blueHighlight,
btnMainBgBasic = blueLight,
btnMainBgHover = blueLighter,
btnMainBgActive = blueMid,
btnMainBgDisabled = blueDisabled,
btnMainText = white,
btnMainTextDisabled = white,
dialogBg = grayDark,
dialogBorder = grayDark,
dialogText = white,
dialogTitleText = white,
emptyOverlay = grayMid & L.a .~ 0.8,
externalLinkBasic = blueLink,
externalLinkHover = blueLighter,
externalLinkFocus = blueLight,
externalLinkActive = blueMid,
externalLinkDisabled = grayDisabled,
iconBg = grayBright,
iconFg = grayDark,
inputIconFg = black,
inputBorder = grayLighter,
inputFocusBorder = blueHighlight,
inputBgBasic = grayInputLight,
inputBgHover = grayLighter,
inputBgFocus = grayInputLight,
inputBgActive = grayLight,
inputBgDisabled = grayDisabled,
inputFgBasic = grayMid,
inputFgHover = grayBright,
inputFgFocus = blueLight,
inputFgActive = blueLighter,
inputFgDisabled = grayMid,
inputSndBasic = grayLighter,
inputSndHover = grayLight,
inputSndFocus = grayMid,
inputSndActive = grayDark,
inputSndDisabled = grayLighter,
inputHlBasic = grayLight,
inputHlHover = grayLighter,
inputHlFocus = blueHighlight,
inputHlActive = blueHighlight,
inputHlDisabled = grayLight,
inputSelBasic = grayMid,
inputSelFocus = blueSelectionLight,
inputText = black,
inputTextDisabled = grayDarker,
labelText = black,
scrollBarBasic = grayDark & L.a .~ 0.2,
scrollThumbBasic = grayMid & L.a .~ 0.6,
scrollBarHover = grayDark & L.a .~ 0.4,
scrollThumbHover = grayMid & L.a .~ 0.8,
slMainBg = grayDarker,
slNormalBgBasic = transparent,
slNormalBgHover = grayDark,
slNormalText = black,
slNormalFocusBorder = blueHighlight,
slSelectedBgBasic = grayMid,
slSelectedBgHover = grayLight,
slSelectedText = black,
slSelectedFocusBorder = blueHighlight,
tooltipBorder = grayLighter,
tooltipBg = grayInputLight,
tooltipText = black
}
darkTheme :: Theme
darkTheme = baseTheme darkMod where
black = rgbHex "#000000"
white = rgbHex "#FFFFFF"
blueSelection = rgbHex "#1414FF"
blueDisabled = rgbHex "#314C69"
blueMid = rgbHex "#4259DB"
blueLight = rgbHex "#4E8CF7"
blueLighter = rgbHex "#63B7FB"
blueHighlight = rgbHex "#87CEFA"
blueHighlightBtn = rgbHex "#BBE2FA"
blueLink = rgbHex "#58A6FF"
grayBg = rgbHex "#2E2E2E"
grayDisabled = rgbHex "#606060"
grayDarker = rgbHex "#282828"
grayDark = rgbHex "#404040"
grayInput = rgbHex "#575757"
grayMid = rgbHex "#6E6E6E"
grayLight = rgbHex "#8C8C8C"
grayLighter = rgbHex "#A4A4A4"
grayBright = rgbHex "#C4C4C4"
grayBrighter = rgbHex "#E4E4E4"
grayBorder = rgbHex "#393939"
darkMod = BaseThemeColors {
clearColor = grayBg,
btnFocusBorder = blueHighlightBtn,
btnBgBasic = grayLighter,
btnBgHover = grayBright,
btnBgActive = grayLight,
btnBgDisabled = grayDisabled,
btnText = grayDarker,
btnTextDisabled = grayDarker,
btnMainFocusBorder = blueHighlight,
btnMainBgBasic = blueLight,
btnMainBgHover = blueLighter,
btnMainBgActive = blueMid,
btnMainBgDisabled = blueDisabled,
btnMainText = white,
btnMainTextDisabled = white,
dialogBg = grayDark,
dialogBorder = grayDark,
dialogText = white,
dialogTitleText = white,
emptyOverlay = grayMid & L.a .~ 0.8,
externalLinkBasic = blueLink,
externalLinkHover = blueLighter,
externalLinkFocus = blueLight,
externalLinkActive = blueMid,
externalLinkDisabled = grayDisabled,
iconBg = grayBright,
iconFg = grayDark,
inputIconFg = black,
inputBorder = grayDarker,
inputFocusBorder = blueHighlight,
inputBgBasic = grayInput,
inputBgHover = grayMid,
inputBgFocus = grayInput,
inputBgActive = grayLight,
inputBgDisabled = grayDisabled,
inputFgBasic = grayMid,
inputFgHover = grayLight,
inputFgFocus = blueLighter,
inputFgActive = blueLighter,
inputFgDisabled = grayMid,
inputSndBasic = grayLighter,
inputSndHover = grayLight,
inputSndFocus = grayMid,
inputSndActive = grayDark,
inputSndDisabled = grayLighter,
inputHlBasic = grayLight,
inputHlHover = grayLighter,
inputHlFocus = blueHighlight,
inputHlActive = blueHighlight,
inputHlDisabled = grayLight,
inputSelBasic = grayMid,
inputSelFocus = blueSelection,
inputText = white,
inputTextDisabled = grayDarker,
labelText = white,
scrollBarBasic = grayDark & L.a .~ 0.2,
scrollThumbBasic = grayMid & L.a .~ 0.6,
scrollBarHover = grayDark & L.a .~ 0.4,
scrollThumbHover = grayMid & L.a .~ 0.8,
slMainBg = grayDarker,
slNormalBgBasic = transparent,
slNormalBgHover = grayDark,
slNormalText = white,
slNormalFocusBorder = blueHighlight,
slSelectedBgBasic = grayMid,
slSelectedBgHover = grayLight,
slSelectedText = white,
slSelectedFocusBorder = blueHighlight,
tooltipBorder = grayLighter,
tooltipBg = grayInput,
tooltipText = white
}
darkTheme = baseTheme darkThemeColors
darkThemeColors :: BaseThemeColors
darkThemeColors = BaseThemeColors {
clearColor = grayBgDark,
btnFocusBorder = blueHighlightBtn,
btnBgBasic = grayLighter,
btnBgHover = grayBright,
btnBgActive = grayLight,
btnBgDisabled = grayDisabled,
btnText = grayDarker,
btnTextDisabled = grayDarker,
btnMainFocusBorder = blueHighlight,
btnMainBgBasic = blueLight,
btnMainBgHover = blueLighter,
btnMainBgActive = blueMid,
btnMainBgDisabled = blueDisabled,
btnMainText = white,
btnMainTextDisabled = white,
dialogBg = grayDark,
dialogBorder = grayDark,
dialogText = white,
dialogTitleText = white,
emptyOverlay = grayMid & L.a .~ 0.8,
externalLinkBasic = blueLink,
externalLinkHover = blueLighter,
externalLinkFocus = blueLight,
externalLinkActive = blueMid,
externalLinkDisabled = grayDisabled,
iconBg = grayBright,
iconFg = grayDark,
inputIconFg = black,
inputBorder = grayDarker,
inputFocusBorder = blueHighlight,
inputBgBasic = grayInputDark,
inputBgHover = grayMid,
inputBgFocus = grayInputDark,
inputBgActive = grayLight,
inputBgDisabled = grayDisabled,
inputFgBasic = grayMid,
inputFgHover = grayLight,
inputFgFocus = blueLighter,
inputFgActive = blueLighter,
inputFgDisabled = grayMid,
inputSndBasic = grayLighter,
inputSndHover = grayLight,
inputSndFocus = grayMid,
inputSndActive = grayDark,
inputSndDisabled = grayLighter,
inputHlBasic = grayLight,
inputHlHover = grayLighter,
inputHlFocus = blueHighlight,
inputHlActive = blueHighlight,
inputHlDisabled = grayLight,
inputSelBasic = grayMid,
inputSelFocus = blueSelectionDark,
inputText = white,
inputTextDisabled = grayDarker,
labelText = white,
scrollBarBasic = grayDark & L.a .~ 0.2,
scrollThumbBasic = grayMid & L.a .~ 0.6,
scrollBarHover = grayDark & L.a .~ 0.4,
scrollThumbHover = grayMid & L.a .~ 0.8,
slMainBg = grayDarker,
slNormalBgBasic = transparent,
slNormalBgHover = grayDark,
slNormalText = white,
slNormalFocusBorder = blueHighlight,
slSelectedBgBasic = grayMid,
slSelectedBgHover = grayLight,
slSelectedText = white,
slSelectedFocusBorder = blueHighlight,
tooltipBorder = grayLighter,
tooltipBg = grayInputDark,
tooltipText = white
}
black = rgbHex "#000000"
white = rgbHex "#FFFFFF"
blueSelectionLight = rgbHex "#63B7FB"
blueSelectionDark = rgbHex "#1414FF"
blueDisabled = rgbHex "#314C69"
blueMid = rgbHex "#4259DB"
blueLight = rgbHex "#4E8CF7"
blueLighter = rgbHex "#63B7FB"
blueHighlight = rgbHex "#87CEFA"
blueHighlightBtn = rgbHex "#BBE2FA"
blueLink = rgbHex "#58A6FF"
grayBgLight = rgbHex "#EAEAEA"
grayBgDark = rgbHex "#2E2E2E"
grayDisabled = rgbHex "#606060"
grayDarker = rgbHex "#282828"
grayDark = rgbHex "#404040"
grayInputLight = rgbHex "#C4C4C4"
grayInputDark = rgbHex "#575757"
grayMid = rgbHex "#6E6E6E"
grayLight = rgbHex "#8C8C8C"
grayLighter = rgbHex "#A4A4A4"
grayBright = rgbHex "#C4C4C4"
grayBrighter = rgbHex "#E4E4E4"
grayBorder = rgbHex "#393939"

View File

@ -777,13 +777,18 @@
- Still on the fence with containers having config before children.
- Check consistency of click to focus. Dial, slider, inputField (should get focus on shift?)
- Simple click receives focus, shift+click allows changing values without getting focus
- Avoid building examples when used as a library.
- Tried using flags and it works, but it ends up being annoying for testing examples with stack run.
Next
- Document themes and how widgets use them.
- Add ignoreStyle to widgets that may need it.
- Maybe spacer should be 8 pixels wide.
- Rethink activeStyle, activeTheme, active style related function names.
- Add hsl function https://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion
- Remove dpr calculations from NanoVGRenderer.
- Same with FontManager.
- When testing Windows/Linux, check if scroll rate needs to be adjusted.
- Document themes and how widgets use them.
- Avoid building examples when used as a library.
- Create ContextMenu (could work similarly to Tooltip).
Future