diff --git a/libraries/Graphics/Input.elm b/libraries/Graphics/Input.elm index 246f60f..0ae2dcf 100644 --- a/libraries/Graphics/Input.elm +++ b/libraries/Graphics/Input.elm @@ -19,14 +19,15 @@ examples in this library, so just read on to get a better idea of how it works! @docs Input, input # Basic Input Elements -Text fields come later. + +To learn about text fields, see the +[`Graphics.Input.Field`](Graphics-Input-Field) library. + @docs button, customButton, checkbox, dropDown # Clicks and Hovers @docs clickable, hoverable -# Text Fields -@docs field, password, email, noContent, FieldContent, Selection, Direction -} import Signal (Signal) @@ -175,57 +176,3 @@ distinguished with IDs or more complex data structures. -} clickable : Handle a -> a -> Element -> Element clickable = Native.Graphics.Input.clickable - -{-| Represents the current content of a text field. For example: - - FieldContent "She sells sea shells" (Selection 0 3 Backward) - -This means the user highlighted the substring `"She"` backwards. --} -type FieldContent = { string:String, selection:Selection } - -{-| The selection within a text field. `start` is never greater than `end`: - - Selection 0 0 Forward -- cursor precedes all characters - - Selection 5 9 Backward -- highlighting characters starting after - -- the 5th and ending after the 9th --} -type Selection = { start:Int, end:Int, direction:Direction } - -{-| The direction of selection.-} -data Direction = Forward | Backward - -{-| A field with no content: - - FieldContent "" (Selection 0 0 Forward) --} -noContent : FieldContent -noContent = FieldContent "" (Selection 0 0 Forward) - -{-| Create a text field. The following example creates a time-varying element -called `nameField`. As the user types their name, the field will be updated -to match what they have entered. - - name : Input FieldContent - name = input noContent - - nameField : Signal Element - nameField = field name.handle id "Name" <~ name.signal --} -field : Handle a -> (FieldContent -> a) -> String -> FieldContent -> Element -field = Native.Graphics.Input.field - -{-| Same as `field` but the UI element blocks out each characters. -} -password : Handle a -> (FieldContent -> a) -> String -> FieldContent -> Element -password = Native.Graphics.Input.password - -{-| Same as `field` but it adds an annotation that this field is for email -addresses. This is helpful for auto-complete and for mobile users who may -get a custom keyboard with an `@` and `.com` button. --} -email : Handle a -> (FieldContent -> a) -> String -> FieldContent -> Element -email = Native.Graphics.Input.email - --- area : Handle a -> (FieldContent -> a) -> Handle b -> ((Int,Int) -> b) -> (Int,Int) -> String -> FieldContent -> Element --- area = Native.Graphics.Input.area diff --git a/libraries/Graphics/Input/Field.elm b/libraries/Graphics/Input/Field.elm new file mode 100644 index 0000000..c88e27b --- /dev/null +++ b/libraries/Graphics/Input/Field.elm @@ -0,0 +1,130 @@ +module Graphics.Input.Field where +{-| This library specifically addresses text fields. It uses the same genral +approach as the [`Graphics.Input`](Graphics-Input) module for describing an +`Input`, so this library focuses on creating and styling text fields. + +# Create Fields +@docs field, password, email + +# Field Content +@docs Content, Selection, Direction, noContent + +# Field Style +@docs Style, Outline, noOutline, Highlight, noHighlight, Dimensions, uniformly +-} + +import Color (Color) +import Color +import Graphics.Element (Element) +import Graphics.Input (Input, Handle) +import Native.Graphics.Input +import Text + +{-| Easily create uniform dimensions: + + uniformly 4 == { left=4, right=4, top=4, bottom=4 } +-} +uniformly : Int -> Dimensions +uniformly n = Dimensions n n n n + +{-| For setting dimensions of a fields padding or border. The left, right, top, +and bottom may all have different sizes. +-} +type Dimensions = { left:Int, right:Int, top:Int, bottom:Int } + +{-| A field can have a outline around it. This lets you set its color, width, +and radius. The radius allows you to round the corners of your field. Set the +width to zero to make it invisible. +-} +type Outline = { color:Color, width:Dimensions, radius:Int } + +{-| An outline with zero width, so you cannot see it. -} +noOutline : Outline +noOutline = Outline Color.grey (uniformly 0) 0 + +{-| When a field is selected, it has an highlight around it by default. Set the +width of the `Highlight` to zero to make it go away. +-} +type Highlight = { color:Color, width:Int } + +{-| An highlight with zero width, so you cannot see it. -} +noHighlight : Highlight +noHighlight = Highlight Color.blue 0 + +{-| Describes the style of a text box. The `style` field describes the style +of the text itself. The `outline` field describes the glowing blue outline that +shows up when the field has focus. Turn off `outline` by setting its width to +zero. The +-} +type Style = + { padding : Dimensions + , outline : Outline + , highlight : Highlight + , style : Text.Style + } + +{-| The default style for a text field. The outline is `Color.grey` with width +1 and radius 2. The highlight is `Color.blue` with width 1, and the default +text color is black. +-} +defaultStyle : Style +defaultStyle = + { padding = uniformly 4 + , outline = Outline Color.grey (uniformly 1) 2 + , highlight = Highlight Color.blue 1 + , style = Text.defaultStyle + } + +{-| Represents the current content of a text field. For example: + + Content "She sells sea shells" (Selection 0 3 Backward) + +This means the user highlighted the substring `"She"` backwards. +-} +type Content = { string:String, selection:Selection } + +{-| The selection within a text field. `start` is never greater than `end`: + + Selection 0 0 Forward -- cursor precedes all characters + + Selection 5 9 Backward -- highlighting characters starting after + -- the 5th and ending after the 9th +-} +type Selection = { start:Int, end:Int, direction:Direction } + +{-| The direction of selection.-} +data Direction = Forward | Backward + +{-| A field with no content: + + Content "" (Selection 0 0 Forward) +-} +noContent : Content +noContent = Content "" (Selection 0 0 Forward) + +{-| Create a text field. The following example creates a time-varying element +called `nameField`. As the user types their name, the field will be updated +to match what they have entered. + + name : Input Content + name = input noContent + + nameField : Signal Element + nameField = field name.handle id "Name" <~ name.signal +-} +field : Handle a -> (Content -> a) -> Style -> String -> Content -> Element +field = Native.Graphics.Input.field + +{-| Same as `field` but the UI element blocks out each characters. -} +password : Handle a -> (Content -> a) -> Style -> String -> Content -> Element +password = Native.Graphics.Input.password + +{-| Same as `field` but it adds an annotation that this field is for email +addresses. This is helpful for auto-complete and for mobile users who may +get a custom keyboard with an `@` and `.com` button. +-} +email : Handle a -> (Content -> a) -> Style -> String -> Content -> Element +email = Native.Graphics.Input.email + +-- area : Handle a -> (Content -> a) -> Handle b -> ((Int,Int) -> b) -> (Int,Int) -> String -> Content -> Element +-- area = Native.Graphics.Input.area diff --git a/libraries/Native/Color.js b/libraries/Native/Color.js index 8c44c98..80aeec0 100644 --- a/libraries/Native/Color.js +++ b/libraries/Native/Color.js @@ -6,6 +6,12 @@ Elm.Native.Color.make = function(elm) { var Utils = Elm.Native.Utils.make(elm); + function toCss(c) { + return (c._3 === 1) + ? ('rgb(' + c._0 + ', ' + c._1 + ', ' + c._2 + ')') + : ('rgba(' + c._0 + ', ' + c._1 + ', ' + c._2 + ', ' + c._3 + ')'); + } + function complement(rgb) { var hsv = toHSV(rgb); hsv.hue = (hsv.hue + 180) % 360; @@ -63,7 +69,8 @@ Elm.Native.Color.make = function(elm) { return elm.Native.Color.values = { hsva:F4(hsva), hsv:F3(hsv), - complement:complement + complement:complement, + toCss:toCss }; -}; \ No newline at end of file +}; diff --git a/libraries/Native/Graphics/Input.js b/libraries/Native/Graphics/Input.js index 60dde71..0094398 100644 --- a/libraries/Native/Graphics/Input.js +++ b/libraries/Native/Graphics/Input.js @@ -8,6 +8,8 @@ Elm.Native.Graphics.Input.make = function(elm) { var Render = ElmRuntime.use(ElmRuntime.Render.Element); var newNode = ElmRuntime.use(ElmRuntime.Render.Utils).newElement; + var toCss = Elm.Native.Color.make(elm).toCss; + var Text = Elm.Native.Text.make(elm); var Signal = Elm.Signal.make(elm); var newElement = Elm.Graphics.Element.make(elm).newElement; var JS = Elm.Native.JavaScript.make(elm); @@ -78,7 +80,7 @@ Elm.Native.Graphics.Input.make = function(elm) { function updateButton(node, oldModel, newModel) { node.elm_signal = newModel.signal; - node.elm_value = nemModel.value; + node.elm_value = newModel.value; var txt = newModel.text; if (oldModel.text !== txt) node.innerHTML = txt; } @@ -215,11 +217,52 @@ Elm.Native.Graphics.Input.make = function(elm) { } } + function updateIfNeeded(css, attribute, latestAttribute) { + if (css[attribute] !== latestAttribute) { + css[attribute] = latestAttribute; + } + } + function cssDimensions(dimensions) { + return dimensions.top + 'px ' + + dimensions.right + 'px ' + + dimensions.bottom + 'px ' + + dimensions.left + 'px'; + } + function updateFieldStyle(css, style) { + updateIfNeeded(css, 'padding', cssDimensions(style.padding)); + + var outline = style.outline; + updateIfNeeded(css, 'border-width', cssDimensions(outline.width)); + updateIfNeeded(css, 'border-color', toCss(outline.color)); + updateIfNeeded(css, 'border-radius', outline.radius + 'px'); + + var highlight = style.highlight; + if (highlight.width === 0) { + css.outline = 'none'; + } else { + updateIfNeeded(css, 'outline-width', highlight.width + 'px'); + updateIfNeeded(css, 'outline-color', toCss(highlight.color)); + } + + var textStyle = style.style; + updateIfNeeded(css, 'color', toCss(textStyle.color)); + if (textStyle.typeface.ctor !== '[]') { + updateIfNeeded(css, 'font-family', Text.toTypefaces(textStyle.typeface)); + } + if (textStyle.height.ctor !== "Nothing") { + updateIfNeeded(css, 'font-size', textStyle.height._0 + 'px'); + } + updateIfNeeded(css, 'font-weight', textStyle.bold ? 'bold' : 'normal'); + updateIfNeeded(css, 'font-style', textStyle.italic ? 'italic' : 'normal'); + if (textStyle.line.ctor !== 'Nothing') { + updateIfNeeded(css, 'text-decoration', Text.toLine(textStyle.line._0)); + } + } + function renderField(model) { var field = newNode('input'); - field.style.border = 'none'; - field.style.outline = 'none'; - field.style.backgroundColor = 'transparent'; + updateFieldStyle(field.style, model.style); + field.style.borderStyle = 'solid'; field.style.pointerEvents = 'auto'; field.type = model.type; @@ -302,6 +345,9 @@ Elm.Native.Graphics.Input.make = function(elm) { } function updateField(field, oldModel, newModel) { + if (oldModel.style !== newModel.style) { + updateFieldStyle(field.style, newModel.style); + } field.elm_signal = newModel.signal; field.elm_handler = newModel.handler; @@ -316,10 +362,16 @@ Elm.Native.Graphics.Input.make = function(elm) { } function mkField(type) { - function field(signal, handler, placeHolder, content) { + function field(signal, handler, style, placeHolder, content) { + var padding = style.padding; + var outline = style.outline.width; + var adjustWidth = padding.left + padding.right + outline.left + outline.right; + var adjustHeight = padding.top + padding.bottom + outline.top + outline.bottom; return A3(newElement, 200, 30, { ctor: 'Custom', - type: type + 'Input', + type: type + 'Field', + adjustWidth: adjustWidth, + adjustHeight: adjustHeight, render: renderField, update: updateField, model: { @@ -327,14 +379,14 @@ Elm.Native.Graphics.Input.make = function(elm) { handler:handler, placeHolder:placeHolder, content:content, + style:style, type:type } }); } - return F4(field); + return F5(field); } - function hoverable(signal, handler, elem) { function onHover(bool) { elm.notify(signal.id, handler(bool)); diff --git a/libraries/Native/String.js b/libraries/Native/String.js index c1c4d0e..0bf7bf0 100644 --- a/libraries/Native/String.js +++ b/libraries/Native/String.js @@ -10,7 +10,6 @@ Elm.Native.String.make = function(elm) { var Maybe = Elm.Maybe.make(elm); var JS = Elm.JavaScript.make(elm); var Utils = Elm.Native.Utils.make(elm); - var show = Elm.Native.Show.make(elm); function isEmpty(str) { return str.length === 0; @@ -244,7 +243,6 @@ Elm.Native.String.make = function(elm) { endsWith: F2(endsWith), indexes: F2(indexes), - show:show, toInt: toInt, toFloat: toFloat, toList: toList, diff --git a/libraries/Native/Text.js b/libraries/Native/Text.js index 2398586..e79680e 100644 --- a/libraries/Native/Text.js +++ b/libraries/Native/Text.js @@ -4,11 +4,10 @@ Elm.Native.Text.make = function(elm) { elm.Native.Text = elm.Native.Text || {}; if (elm.Native.Text.values) return elm.Native.Text.values; - var JS = Elm.JavaScript.make(elm); - var Utils = Elm.Native.Utils.make(elm); - var Color = Elm.Native.Color.make(elm); + var toCss = Elm.Native.Color.make(elm).toCss; var Element = Elm.Graphics.Element.make(elm); - var show = Elm.Native.Show.make(elm).show; + var List = Elm.Native.List.make(elm); + var Utils = Elm.Native.Utils.make(elm); function makeSpaces(s) { if (s.length == 0) { return s; } @@ -51,13 +50,51 @@ Elm.Native.Text.make = function(elm) { return arr.join('
'); } - function toText(str) { return Utils.txt(properEscape(JS.fromString(str))); } + function toText(str) { return Utils.txt(properEscape(str)); } + // conversions from Elm values to CSS + function toTypefaces(list) { + var typefaces = List.toArray(list); + for (var i = typefaces.length; i--; ) { + var typeface = typefaces[i]; + if (typeface.contains(' ')) { + typefaces[i] = "'" + typeface + "'"; + } + } + return typefaces.join(','); + } + function toLine(line) { + var ctor = line.ctor; + return ctor === 'Under' ? 'underline' : + ctor === 'Over' ? 'overline' : 'line-through'; + } + + // setting styles of Text + function style(style, text) { + var newText = '' + return Utils.txt(newText); + } function height(px, text) { return { style: 'font-size:' + px + 'px;', text:text } } - function typeface(name, text) { - return { style: 'font-family:' + name + ';', text:text } + function typeface(names, text) { + return { style: 'font-family:' + toTypefaces(names) + ';', text:text } } function monospace(text) { return { style: 'font-family:monospace;', text:text } @@ -71,25 +108,16 @@ Elm.Native.Text.make = function(elm) { function link(href, text) { return { href: toText(href), text:text }; } - function underline(text) { - return { line: ' underline', text:text }; - } - function overline(text) { - return { line: ' overline', text:text }; - } - function strikeThrough(text) { - return { line: ' line-through', text:text }; + function line(line, text) { + return { style: 'text-decoration:' + toLine(line) + ';', text:text }; } - function color(c, text) { - var color = (c._3 === 1) - ? ('rgb(' + c._0 + ', ' + c._1 + ', ' + c._2 + ')') - : ('rgba(' + c._0 + ', ' + c._1 + ', ' + c._2 + ', ' + c._3 + ')'); - return { style: 'color:' + color + ';', text:text }; + function color(color, text) { + return { style: 'color:' + toCss(color) + ';', text:text }; } - function position(align) { - function create(text) { + function block(align) { + return function(text) { var raw = { ctor :'RawHtml', html : Utils.makeText(text), @@ -100,7 +128,6 @@ Elm.Native.Text.make = function(elm) { var pos = A2(Utils.htmlHeight, 0, raw); return A3(Element.newElement, pos._0, pos._1, raw); } - return create; } function markdown(text, guid) { @@ -115,36 +142,25 @@ Elm.Native.Text.make = function(elm) { return A3(Element.newElement, pos._0, pos._1, raw); } - var text = position('left'); - function asText(v) { - return text(monospace(toText(show(v)))); - } - - function plainText(v) { - return text(toText(v)); - } - return elm.Native.Text.values = { toText: toText, height : F2(height), italic : italic, bold : bold, - underline : underline, - overline : overline, - strikeThrough : strikeThrough, + line : F2(line), monospace : monospace, typeface : F2(typeface), color : F2(color), link : F2(link), - justified : position('justify'), - centered : position('center'), - righted : position('right'), - text : text, - plainText : plainText, - markdown : markdown, + leftAligned : block('left'), + rightAligned : block('right'), + centered : block('center'), + justified : block('justify'), + markdown : markdown, - asText : asText, + toTypefaces:toTypefaces, + toLine:toLine, }; }; diff --git a/libraries/Native/Utils.js b/libraries/Native/Utils.js index e59bab0..51ac6ef 100644 --- a/libraries/Native/Utils.js +++ b/libraries/Native/Utils.js @@ -76,14 +76,8 @@ Elm.Native.Utils.make = function(elm) { function makeText(text) { var style = ''; - var line = ''; var href = ''; while (true) { - if (text.line) { - line += text.line; - text = text.text; - continue; - } if (text.style) { style += text.style; text = text.text; @@ -95,7 +89,6 @@ Elm.Native.Utils.make = function(elm) { continue; } if (href) text = '' + text + ''; - if (line) style += 'text-decoration:' + line + ';'; if (style) text = '' + text + ''; return text; } diff --git a/libraries/String.elm b/libraries/String.elm index 37faf99..586cc4c 100644 --- a/libraries/String.elm +++ b/libraries/String.elm @@ -28,6 +28,7 @@ Cosmetic operations such as padding with extra characters or trimming whitespace @docs map, filter, foldl, foldr, any, all -} +import Native.Show import Native.String import Maybe (Maybe) @@ -284,7 +285,7 @@ indices = Native.String.indexes show [1,2] == "[1,2]" -} show : a -> String -show = Native.String.show +show = Native.Show.show {-| Try to convert a string into an int, failing on improperly formatted strings. diff --git a/libraries/Text.elm b/libraries/Text.elm index 37d180a..3e5b6df 100644 --- a/libraries/Text.elm +++ b/libraries/Text.elm @@ -6,94 +6,132 @@ module Text where @docs toText # Creating Elements -@docs plainText, asText, text, centered, justified, righted +@docs leftAligned, rightAligned, centered, justified -# Formatting -@docs color, typeface, height, link +# Links and Style +@docs link, Style, style, Line, defaultStyle -# Simple Formatting -@docs monospace, bold, italic, underline, overline, strikeThrough +# Convenience Functions + +There are two convenience functions for creating an `Element` which can be +useful when debugging or prototyping: + +@docs plainText, asText + +There are also a bunch of functions to set parts of a `Style` individually: + +@docs typeface, monospace, height, color, bold, italic, line -} import Basics (..) -import Color (Color) +import String +import Color (Color, black) import Graphics.Element (Element, Three, Pos, ElementPrim, Properties) -import Maybe (Maybe) +import Maybe (Maybe, Nothing) import JavaScript (JSString) +import Native.Show import Native.Text data Text = Text +data Line = Under | Over | Through + +{-| Representation of all the ways you can style `Text`. +-} +type Style = + { typeface : [String] + , height : Maybe Float + , color : Color + , bold : Bool + , italic : Bool + , line : Maybe Line + } + +{-| Plain black text. It uses the browsers default typeface and text height. +No decorations are used: + + { typeface = [] + , height = Nothing + , color = black + , bold = False + , italic = False + , line = Nothing + } +-} +defaultStyle : Style +defaultStyle = + { typeface = [] + , height = Nothing + , color = black + , bold = False + , italic = False + , line = Nothing + } + {-| Convert a string into text which can be styled and displayed. -} toText : String -> Text toText = Native.Text.toText -{-| Set the typeface of some text. The first argument should be a comma -separated listing of the desired typefaces: - - "helvetica, arial, sans-serif" - -Works the same as the CSS font-family property. +{-| Set the style of some text. -} -typeface : String -> Text -> Text +style : Style -> Text -> Text +style = Native.Text.style + +{-| Provide a list of prefered typefaces for some text. + + ["helvetica","arial","sans-serif"] + +Not everyone has access to the same typefaces, so rendering will use the first +typeface in the list that is found on the user's computer. If there are no +matches, it will use their default typeface. Works the same as the CSS +font-family property. +-} +typeface : [String] -> Text -> Text typeface = Native.Text.typeface {-| Switch to a monospace typeface. Good for code snippets. -} monospace : Text -> Text monospace = Native.Text.monospace -{-| Create a link. -} +{-| Create a link. + + link "http://elm-lang.org" (toText "Elm Website") +-} link : String -> Text -> Text link = Native.Text.link -{-| Set the height of text in pixels. -} height : Float -> Text -> Text height = Native.Text.height -{-| Set the color of a string. -} color : Color -> Text -> Text color = Native.Text.color -{-| Make a string bold. -} bold : Text -> Text bold = Native.Text.bold -{-| Italicize a string. -} italic : Text -> Text italic = Native.Text.italic -{-| Draw a line above a string. -} -overline : Text -> Text -overline = Native.Text.overline +line : Line -> Text -> Text +line = Native.Text.line -{-| Underline a string. -} -underline : Text -> Text -underline = Native.Text.underline +leftAligned : Text -> Element +leftAligned = Native.Text.leftAligned -{-| Draw a line through a string. -} -strikeThrough : Text -> Text -strikeThrough = Native.Text.strikeThrough +rightAligned : Text -> Element +rightAligned = Native.Text.rightAligned -{-| Display justified, styled text. -} -justified : Text -> Element -justified = Native.Text.justified - -{-| Display centered, styled text. -} centered : Text -> Element centered = Native.Text.centered -{-| Display right justified, styled text. -} -righted : Text -> Element -righted = Native.Text.righted - -{-| Display styled text. -} -text : Text -> Element -text = Native.Text.text +justified : Text -> Element +justified = Native.Text.justified {-| Display a plain string. -} plainText : String -> Element -plainText = Native.Text.plainText +plainText str = + leftAligned (toText str) {-| for internal use only -} markdown : Element @@ -107,4 +145,5 @@ the browser: Excellent for debugging. -} asText : a -> Element -asText = Native.Text.asText +asText value = + leftAligned (monospace (toText (Native.Show.show value))) diff --git a/runtime/Init.js b/runtime/Init.js index fd96d76..cc92a61 100644 --- a/runtime/Init.js +++ b/runtime/Init.js @@ -91,7 +91,7 @@ function init(display, container, module, ports, moduleToReplace) { checkPorts(elm); } catch(e) { var directions = "
    Open the developer console for more details." - Module.main = Elm.Text.make(elm).text('' + e.message + directions + ''); + Module.main = Elm.Text.make(elm).leftAligned('' + e.message + directions + ''); reportAnyErrors = function() { throw e; } } inputs = ElmRuntime.filterDeadInputs(inputs); diff --git a/runtime/Render/Element.js b/runtime/Render/Element.js index c472752..a93afbb 100644 --- a/runtime/Render/Element.js +++ b/runtime/Render/Element.js @@ -7,7 +7,15 @@ var newElement = Utils.newElement, extract = Utils.extract, addTransform = Utils.addTransform, removeTransform = Utils.removeTransform, fromList = Utils.fromList, eq = Utils.eq; -function setProps(props, e) { +function setProps(elem, e) { + var props = elem.props; + var element = elem.element; + if (element.adjustWidth) { + props.width -= element.adjustWidth; + } + if (element.adjustHeight) { + props.height -= element.adjustHeight; + } e.style.width = (props.width |0) + 'px'; e.style.height = (props.height|0) + 'px'; if (props.opacity !== 1) { e.style.opacity = props.opacity; } @@ -200,7 +208,7 @@ function rawHtml(elem) { return div; } -function render(elem) { return setProps(elem.props, makeElement(elem)); } +function render(elem) { return setProps(elem, makeElement(elem)); } function makeElement(e) { var elem = e.element; switch(elem.ctor) { @@ -308,7 +316,16 @@ function update(node, curr, next) { } function updateProps(node, curr, next) { - var props = next.props, currP = curr.props, e = node; + var props = next.props; + var currP = curr.props; + var e = node; + var element = next.element; + if (element.adjustWidth) { + props.width -= element.adjustWidth; + } + if (element.adjustHeight) { + props.height -= element.adjustHeight; + } if (props.width !== currP.width) e.style.width = (props.width |0) + 'px'; if (props.height !== currP.height) e.style.height = (props.height|0) + 'px'; if (props.opacity !== 1 && props.opacity !== currP.opacity) {