Bot-Man-Toe/elm/Layout.elm

516 lines
12 KiB
Elm

module Layout exposing
( twoBlocks
, tab, sideIconBar
, iconAsElement, iconAsIcon
, containedButton, outlinedButton, textButton
, textInput, passwordInput
, header, stdText
, itemWithSubtext
, sideList, radioButtons
, loadingIndicator, svg
)
{-|
# Layout
The layout module exposes some boilerplate functions that have produce a
beautiful Material design Elm webpage.
## Screen layout
@docs twoBlocks
## Elements
@docs tab, sideIconBar
## Icons
@docs iconAsElement, iconAsIcon
## Buttons
@docs containedButton, outlinedButton, textButton
## Text fields
@docs textInput, passwordInput
## Text
@docs header, stdText
## Items in a list
@docs itemWithSubtext
## Lists
@docs sideList, radioButtons
## Other elements
@docs loadingIndicator, svg
-}
import Color exposing (Color)
import Element exposing (Element)
import Element.Background
import Element.Events
import Element.Font
import Element.Input
import Html.Attributes
import Material.Icons.Types
import Svg exposing (Svg)
import Svg.Attributes
import Theme
import Widget
import Widget.Customize as Customize
import Widget.Icon exposing (Icon)
import Widget.Material as Material
import Widget.Material.Typography
{-| A contained button representing the most important action of a group.
-}
containedButton :
{ buttonColor : Color
, clickColor : Color
, icon : Icon msg
, onPress : Maybe msg
, text : String
}
-> Element msg
containedButton data =
Widget.button
({ primary = data.buttonColor, onPrimary = data.clickColor }
|> singlePalette
|> Material.containedButton
|> Customize.elementButton [ Element.width Element.fill ]
)
{ text = data.text, icon = data.icon, onPress = data.onPress }
header : String -> Element msg
header =
Element.text
>> Element.el Widget.Material.Typography.h1
>> List.singleton
>> Element.paragraph []
iconAsElement :
{ color : Color
, height : Int
, icon : Material.Icons.Types.Icon msg
, width : Int
}
-> Element msg
iconAsElement data =
data.icon
|> iconAsIcon
|> (|>) { size = Basics.min data.height data.width, color = data.color }
|> Element.el [ Element.centerX, Element.centerY ]
|> Element.el
[ Element.height (Element.px data.height)
, Element.width (Element.px data.width)
]
iconAsIcon : Material.Icons.Types.Icon msg -> Widget.Icon.Icon msg
iconAsIcon =
Widget.Icon.elmMaterialIcons Material.Icons.Types.Color
{-| Multiline item
-}
itemWithSubtext :
{ color : Color
, leftIcon : Widget.Icon.Icon msg
, onPress : Maybe msg
, rightIcon : Widget.Icon.Icon msg
, text : String
, title : String
}
-> Widget.Item msg
itemWithSubtext data =
Widget.multiLineItem
({ primary = data.color, onPrimary = data.color }
|> singlePalette
|> Material.multiLineItem
)
{ content = data.rightIcon
, icon = data.leftIcon
, onPress = data.onPress
, title = data.title
, text = data.text
}
{-| Circular loading bar indicator
-}
loadingIndicator :
{ color : Color
}
-> Element msg
loadingIndicator data =
Widget.circularProgressIndicator
({ primary = data.color, onPrimary = data.color }
|> singlePalette
|> Material.progressIndicator
)
Nothing
{-| An outlined button representing an important action within a group.
-}
outlinedButton :
{ color : Color
, icon : Icon msg
, onPress : Maybe msg
, text : String
}
-> Element msg
outlinedButton data =
Widget.button
({ primary = data.color, onPrimary = data.color }
|> singlePalette
|> Material.outlinedButton
)
{ text = data.text, icon = data.icon, onPress = data.onPress }
{-| Show a password field
-}
passwordInput :
{ color : Color
, label : String
, onChange : String -> msg
, placeholder : Maybe String
, show : Bool
, text : String
}
-> Element msg
passwordInput data =
Widget.currentPasswordInputV2
({ primary = data.color, onPrimary = data.color }
|> singlePalette
|> Material.passwordInput
|> Customize.elementRow [ Element.width Element.fill ]
)
{ label = data.label
, onChange = data.onChange
, placeholder =
data.placeholder
|> Maybe.map Element.text
|> Maybe.map (Element.Input.placeholder [])
, show = data.show
, text = data.text
}
{-| Redio buttons are side-by-side buttons that only allowed up to one to be
selected.
-}
radioButtons :
{ color : Color
, items : List ( Bool, a )
, toIcon : a -> Icon msg
, toString : a -> String
, onChange : a -> msg
}
-> Element msg
radioButtons data =
data.items
|> List.map
(Tuple.mapSecond
(\item ->
{ text = data.toString item
, icon =
\{ size, color } ->
Element.text (data.toString item)
, onPress = Just (data.onChange item)
}
)
)
|> Widget.toggleRow
{ elementRow = Material.toggleRow
, content =
{ primary = data.color, onPrimary = data.color }
|> singlePalette
|> Material.toggleButton
}
{-| Create a simple palette.
-}
singlePalette : { primary : Color, onPrimary : Color } -> Material.Palette
singlePalette { primary, onPrimary } =
{ primary = primary
, secondary = primary
, background = primary
, surface = primary
, error = primary
, on =
{ primary = onPrimary
, secondary = onPrimary
, background = onPrimary
, surface = onPrimary
, error = onPrimary
}
}
sideIconBar :
{ colorBackground : Color
, colorText : Color
, height : Int
, items : List { icon : Widget.Icon.Icon msg, onPress : msg, text : String }
, width : Int
}
-> Element msg
sideIconBar data =
let
buttonHeight =
round (toFloat data.width * 1.618)
fontSize =
data.width // 6
iconSize =
data.width * 3 // 5
in
data.items
|> List.map
(\item ->
[ item.icon { size = iconSize, color = data.colorText }
|> Element.el [ Element.centerX ]
, Element.paragraph [] [ Element.text item.text ]
]
|> Element.column
[ Element.centerX
, Element.centerY
, Element.Font.bold
, Element.Font.center
, Element.Font.size fontSize
, Element.htmlAttribute (Html.Attributes.style "cursor" "pointer")
]
|> Element.el
[ Element.centerY
, Element.Events.onClick item.onPress
, Element.height (Element.px data.width)
, Element.width (Element.px data.width)
]
|> Element.el
[ Element.height (Element.px buttonHeight)
]
)
|> Element.column
[ Element.Background.color (Theme.toElmUiColor data.colorBackground)
, Element.height (Element.px data.height)
, Element.scrollbarY
, Element.width (Element.px data.width)
]
sideList : { color : Color, items : List (Widget.Item msg), width : Int } -> Element msg
sideList data =
let
width px =
Element.width (Element.px px)
in
Widget.itemList
({ primary = data.color, onPrimary = data.color }
|> singlePalette
|> Material.sideSheet
)
data.items
|> Element.el [ Element.centerX, width (Basics.min 360 data.width) ]
|> Element.el [ width data.width ]
stdText : String -> Element msg
stdText =
Element.text >> List.singleton >> Element.paragraph []
svg :
{ aspectRatio : Float
, height : Int
, svg : Svg msg
, width : Int
, viewMinX : Float
, viewMaxX : Float
, viewMinY : Float
, viewMaxY : Float
}
-> Element msg
svg data =
let
givenWidth =
toFloat data.width
givenHeight =
toFloat data.height
scaleFactorWidth =
givenHeight / givenWidth
innerWidth =
if scaleFactorWidth > data.aspectRatio then
givenWidth
else
givenHeight / data.aspectRatio
innerHeight =
if scaleFactorWidth > data.aspectRatio then
givenWidth * data.aspectRatio
else
givenHeight
in
Svg.svg
[ [ data.viewMinX, data.viewMinY, data.viewMaxX - data.viewMinX, data.viewMaxY - data.viewMinY ]
|> List.map String.fromFloat
|> String.join " "
|> Svg.Attributes.viewBox
, Svg.Attributes.width (String.fromFloat innerWidth)
, Svg.Attributes.height (String.fromFloat innerHeight)
]
[ data.svg ]
|> Element.html
|> Element.el [ Element.centerX, Element.centerY ]
|> Element.el
[ Element.height (Element.px data.height)
, Element.width (Element.px data.width)
]
{-| A tab selector that always has an item selected.
-}
tab :
{ color : Color
, content : Int -> Element msg
, items : List { text : String, icon : Icon msg }
, onSelect : Int -> msg
, selected : Int
}
-> Element msg
tab data =
Widget.tab
({ primary = data.color, onPrimary = data.color }
|> singlePalette
|> Material.tab
)
{ tabs =
{ onSelect = data.onSelect >> Just
, options = data.items
, selected = Just data.selected
}
, content = \_ -> data.content data.selected
}
{-| A text button representing an important action within a group.
-}
textButton :
{ icon : Icon msg
, onPress : Maybe msg
, text : String
, color : Color
}
-> Element msg
textButton data =
Widget.button
({ primary = data.color, onPrimary = data.color }
|> singlePalette
|> Material.textButton
)
{ text = data.text, icon = data.icon, onPress = data.onPress }
{-| Text input element.
-}
textInput :
{ color : Color
, label : String
, onChange : String -> msg
, placeholder : Maybe String
, text : String
}
-> Element msg
textInput data =
Widget.textInput
({ primary = data.color, onPrimary = data.color }
|> singlePalette
|> Material.textInput
|> Customize.elementRow [ Element.width Element.fill ]
)
{ chips = []
, text = data.text
, placeholder =
data.placeholder
|> Maybe.map Element.text
|> Maybe.map (Element.Input.placeholder [])
, label = data.label
, onChange = data.onChange
}
{-| Two blocks either next to each other or below each other, depending on the
screen shape.
-}
twoBlocks :
{ height : Int
, el1 : { height : Int, width : Int } -> Element msg
, el2 : { height : Int, width : Int } -> Element msg
, width : Int
}
-> Element msg
twoBlocks data =
let
goesVertical =
2 * data.width <= 3 * data.height
direction =
if goesVertical then
Element.column
else
Element.row
width =
if goesVertical then
data.width
else
data.width // 2
height =
if goesVertical then
data.height // 2
else
data.height
in
direction
[ Element.height (Element.px data.height)
, Element.width (Element.px data.width)
]
[ data.el1 { height = height, width = width }
, data.el2 { height = height, width = width }
]