| -- 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 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 News |
| import Ports |
| |
| |
| |
| ------------- MODEL ------------------------------------------------------------ |
| |
| |
| type alias Model = |
| { activeInput : Mode |
| , inputMap : Mode.Map InputState |
| } |
| |
| |
| type alias InputState = |
| { activeOutput : Mode |
| , outputMap : Mode.Map OutputState |
| } |
| |
| |
| type alias OutputState = |
| { options : Options |
| } |
| |
| |
| type alias Options = |
| Form.Model |
| |
| |
| type alias Active = |
| { input : Mode |
| , output : Mode |
| , options : 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 |
| { 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 |
| |
| options = |
| outputState.options |
| in |
| Active input output 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/output mode combination. |
| | UpdateOptions 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 |
| |
| UpdateOptions formMsg -> |
| updateOptions (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 } |
| |
| |
| updateOptions : (Options -> Options) -> Model -> Model |
| updateOptions 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.encodeEffective |
| (Mode.options active.input active.output) |
| active.options |
| ) |
| |
| |
| |
| ------------- 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, optionsPart } = |
| case editor of |
| Input -> |
| { modes = Mode.inputs |
| , activeMode = active.input |
| , msg = ActivateInput |
| , id = "InputEditor" |
| , optionsPart = [] |
| } |
| |
| Output -> |
| let |
| form = |
| Mode.options active.input active.output |
| in |
| { modes = Mode.outputs active.input |
| , activeMode = active.output |
| , msg = ActivateOutput |
| , id = "OutputEditor" |
| , optionsPart = |
| if List.isEmpty form.inputs then |
| [] |
| |
| else |
| [ optionsView form active.options toMsg ] |
| } |
| |
| navbarPart = |
| navbarView modes activeMode (toMsg << msg) themeClass |
| |
| editorPart = |
| Html.div |
| [ Attributes.id id |
| , Attributes.class "editor" |
| ] |
| [] |
| 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 -> (Msg -> msg) -> Html msg |
| optionsView form options toMsg = |
| Html.div |
| [ Attributes.class "editor-options" ] |
| [ Form.view |
| form |
| [] |
| (toMsg << UpdateOptions) |
| 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 |
| [ ( "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)) |
| |
| |
| decodeInputState : Decode.Decoder InputState |
| decodeInputState = |
| Decode.map2 InputState |
| (Decode.field "activeOutput" Mode.decode) |
| (Decode.field "outputMap" (Mode.decodeMap decodeOutputState)) |
| |
| |
| decodeOutputState : Decode.Decoder OutputState |
| decodeOutputState = |
| Decode.map OutputState |
| (Decode.field "options" Form.decode) |