blob: 8f6faa417c9b736550d8a51abf52cbb26bbd5546 [file] [log] [blame] [edit]
-- Copyright 2020 The Fuchsia Authors. All rights reserved.
-- Use of this source code is governed by a BSD-style license that can be
-- found in the LICENSE file.
module Editors exposing
( Editor(..)
, Model
, Msg
, decode
, encode
, init
, initCmd
, update
, view
)
{-| This module implements the input/output editors.
Input and output editors are not independent. Anything to do with an output mode
-- its active/nonactive state, its content, its options -- is always relative to
an input mode. For example, FIDL Text -> Bytes and Bytes -> Bytes have distinct
output state even though both are Bytes.
This module does not keep track of the Ace editor state. That is done in
JavaScript-land because it's easier there.
-}
import Dict
import Form exposing (Form)
import Html exposing (Html)
import Html.Attributes as Attributes
import Html.Events as Events
import Json.Decode as Decode
import Json.Encode as Encode
import Mode exposing (Mode)
import Ports
------------- MODEL ------------------------------------------------------------
type alias Model =
{ activeInput : Mode
, inputMap : Mode.Map InputState
}
type alias InputState =
{ options : Options
, activeOutput : Mode
, outputMap : Mode.Map OutputState
}
type alias OutputState =
{ options : Options
}
type alias Options =
Form.Model
type alias Active =
{ input : Mode
, output : Mode
, inputOptions : Options
, inputOutputOptions : Options
}
init : Model
init =
let
input =
Mode.defaultInput
inputState =
defaultInputState input
in
{ activeInput = input
, inputMap = Mode.singleton input inputState
}
initCmd : Model -> Cmd msg
initCmd model =
updateEditors (getActive model)
defaultInputState : Mode -> InputState
defaultInputState input =
let
output =
Mode.defaultOutput input
outputState =
defaultOutputState input output
in
{ options = Form.empty
, activeOutput = output
, outputMap = Mode.singleton output outputState
}
defaultOutputState : Mode -> Mode -> OutputState
defaultOutputState input output =
{ options = Form.empty }
getInputState : Mode -> Model -> InputState
getInputState input model =
Maybe.withDefault
(defaultInputState input)
(Mode.get input model.inputMap)
getOutputState : Mode -> Mode -> InputState -> OutputState
getOutputState input output inputState =
Maybe.withDefault
(defaultOutputState input output)
(Mode.get output inputState.outputMap)
getActive : Model -> Active
getActive model =
let
input =
model.activeInput
inputState =
getInputState input model
output =
inputState.activeOutput
outputState =
getOutputState input output inputState
in
Active input output inputState.options outputState.options
------------- UPDATE -----------------------------------------------------------
type Msg
= NoOp
| -- Switch to this input mode. This also implies switching to a new output
-- mode (the one that was most recently used for that input).
ActivateInput Mode
-- Switch to this output mode, keeping the same input mode.
| ActivateOutput Mode
-- Update options for the current input mode.
| UpdateInputOptions Form.Msg
-- Update options for the current input/output mode combination.
| UpdateInputOutputOptions Form.Msg
update : Msg -> Model -> ( Model, Cmd msg )
update msg model =
let
newModel =
case msg of
NoOp ->
model
ActivateInput input ->
activateInput input model
ActivateOutput output ->
activateOutput output model
UpdateInputOptions formMsg ->
updateInputOptions (Form.update formMsg) model
UpdateInputOutputOptions formMsg ->
updateInputOutputOptions (Form.update formMsg) model
cmd =
case msg of
NoOp ->
Cmd.none
_ ->
updateEditors (getActive newModel)
in
( newModel, cmd )
activateInput : Mode -> Model -> Model
activateInput input model =
let
inputState =
getInputState input model
newInputMap =
model.inputMap |> Mode.insert input inputState
in
{ model | activeInput = input, inputMap = newInputMap }
activateOutput : Mode -> Model -> Model
activateOutput output model =
let
input =
model.activeInput
inputState =
getInputState input model
outputState =
getOutputState input output inputState
newOutputMap =
inputState.outputMap |> Mode.insert output outputState
newInputState =
{ inputState | activeOutput = output, outputMap = newOutputMap }
newInputMap =
model.inputMap |> Mode.insert input newInputState
in
{ model | inputMap = newInputMap }
updateInputOptions : (Options -> Options) -> Model -> Model
updateInputOptions alter model =
let
input =
model.activeInput
inputState =
getInputState input model
newInputState =
{ inputState | options = alter inputState.options }
newInputMap =
model.inputMap |> Mode.insert input newInputState
in
{ model | inputMap = newInputMap }
updateInputOutputOptions : (Options -> Options) -> Model -> Model
updateInputOutputOptions alter model =
let
input =
model.activeInput
inputState =
getInputState input model
output =
inputState.activeOutput
outputState =
getOutputState input output inputState
newOutputState =
{ options = alter outputState.options }
newOutputMap =
inputState.outputMap |> Mode.insert output newOutputState
newInputState =
{ inputState | outputMap = newOutputMap }
newInputMap =
model.inputMap |> Mode.insert input newInputState
in
{ model | inputMap = newInputMap }
updateEditors : Active -> Cmd msg
updateEditors active =
Ports.updateEditors
(Mode.encode active.input)
(Mode.encode active.output)
(Form.encodeEffectiveCombined
[ ( Mode.inputOptions active.input
, active.inputOptions
)
, ( Mode.inputOutputOptions active.input active.output
, active.inputOutputOptions
)
]
)
------------- VIEW -------------------------------------------------------------
type Editor
= Input
| Output
view : Editor -> (Msg -> msg) -> String -> Model -> Html msg
view editor toMsg themeClass model =
let
active =
getActive model
{ modes, activeMode, msg, id, form, options, updateOptions } =
case editor of
Input ->
{ modes = Mode.inputs
, activeMode = active.input
, msg = ActivateInput
, id = "InputEditor"
, form = Mode.inputOptions active.input
, options = active.inputOptions
, updateOptions = UpdateInputOptions
}
Output ->
{ modes = Mode.outputs active.input
, activeMode = active.output
, msg = ActivateOutput
, id = "OutputEditor"
, form = Mode.inputOutputOptions active.input active.output
, options = active.inputOutputOptions
, updateOptions = UpdateInputOutputOptions
}
navbarPart =
navbarView modes activeMode (toMsg << msg) themeClass
editorPart =
Html.div
[ Attributes.id id
, Attributes.class "editor"
]
[]
optionsPart =
if List.isEmpty form.inputs then
[]
else
[ optionsView form options (toMsg << updateOptions) ]
in
Html.div
[ Attributes.class "editor-wrapper"
, Events.on "keydown" (keyDownEvent editor active toMsg)
]
(navbarPart :: editorPart :: optionsPart)
navbarView : List Mode -> Mode -> (Mode -> msg) -> String -> Html msg
navbarView modes activeMode msg themeClass =
let
navItem mode =
-- We provide keyboard shortcuts on the parent div to navigate
-- between modes. This replaces tab navigation, hence tabindex -1.
-- We need to use button tags so that they can be focused and the
-- parent div will receive keyboard events.
Html.button
[ Attributes.type_ "button"
, Attributes.tabindex -1
, Attributes.class "navbar-item"
, if mode == activeMode then
Attributes.class ("navbar-item--active " ++ themeClass)
else
Events.onClick (msg mode)
]
[ Html.text (Mode.title mode) ]
in
Html.div
[ Attributes.class "navbar" ]
(List.map navItem modes)
optionsView : Form -> Form.Model -> (Form.Msg -> msg) -> Html msg
optionsView form options toMsg =
Html.div
[ Attributes.class "editor-options" ]
[ Form.view
form
[]
toMsg
options
]
keyDownEvent : Editor -> Active -> (Msg -> msg) -> Decode.Decoder msg
keyDownEvent editor active toMsg =
let
( activate, previous, next ) =
case editor of
Input ->
( ActivateInput
, Mode.previousInput active.input
, Mode.nextInput active.input
)
Output ->
( ActivateOutput
, Mode.previousOutput active.input active.output
, Mode.nextOutput active.input active.output
)
chooseMsg ctrl key =
if ctrl && key == "[" then
activate previous
else if ctrl && key == "]" then
activate next
else
NoOp
in
Decode.map toMsg <|
Decode.map2 chooseMsg
(Decode.field "ctrlKey" Decode.bool)
(Decode.field "key" Decode.string)
------------- ENCODE / DECODE --------------------------------------------------
encode : Model -> Encode.Value
encode model =
Encode.object
[ ( "activeInput", Mode.encode model.activeInput )
, ( "inputMap", Mode.encodeMap encodeInputState model.inputMap )
]
encodeInputState : InputState -> Encode.Value
encodeInputState inputState =
Encode.object
[ ( "options", Form.encode inputState.options )
, ( "activeOutput", Mode.encode inputState.activeOutput )
, ( "outputMap", Mode.encodeMap encodeOutputState inputState.outputMap )
]
encodeOutputState : OutputState -> Encode.Value
encodeOutputState outputState =
Encode.object
[ ( "options", Form.encode outputState.options ) ]
decode : Decode.Decoder Model
decode =
Decode.map2 Model
(Decode.field "activeInput" Mode.decode)
(Decode.field "inputMap" (Mode.decodeMap decodeInputState))
|> Decode.map conform
decodeInputState : Decode.Decoder InputState
decodeInputState =
Decode.map3 InputState
(Decode.field "options" Form.decodeWithoutConforming)
(Decode.field "activeOutput" Mode.decode)
(Decode.field "outputMap" (Mode.decodeMap decodeOutputState))
decodeOutputState : Decode.Decoder OutputState
decodeOutputState =
Decode.map OutputState
(Decode.field "options" Form.decodeWithoutConforming)
conform : Model -> Model
conform model =
let
newModel =
{ model | inputMap = newInputMap }
newInputMap =
Dict.map conformInputMap model.inputMap
conformInputMap inputModeStr inputState =
case Mode.fromString inputModeStr of
Just inputMode ->
conformInputState inputMode inputState
Nothing ->
inputState
conformInputState inputMode inputState =
let
form =
Mode.inputOptions inputMode
newOutputMap =
Dict.map (conformOutputMap inputMode) inputState.outputMap
in
{ inputState
| options = Form.conform form inputState.options
, outputMap = newOutputMap
}
conformOutputMap inputMode outputModeStr outputState =
case Mode.fromString outputModeStr of
Just outputMode ->
conformOutputState inputMode outputMode outputState
Nothing ->
outputState
conformOutputState inputMode outputMode outputState =
let
form =
Mode.inputOutputOptions inputMode outputMode
in
{ outputState | options = Form.conform form outputState.options }
in
newModel