Move layout into settings

This removes the "Layout" button (which was confusingly short for
"toggle horizontal/vertical layout"), replacing it with a dropdown in
settings that can be Auto (default), Horizontal, or Vertical. The Auto
layout is horizontal for narrow windows and vertical for wide windows.

I also moved the Help button to the top right because I think it looks
better that way.

Test: tested manually
Change-Id: I75eac5545594f484371f228bc8431e5f64c3af9b
Reviewed-on: https://fuchsia-review.googlesource.com/c/fidlbolt/+/1066202
Reviewed-by: Ian McKellar <ianloic@google.com>
diff --git a/frontend/src/elm/Main.elm b/frontend/src/elm/Main.elm
index 6c515a2..a446b71 100644
--- a/frontend/src/elm/Main.elm
+++ b/frontend/src/elm/Main.elm
@@ -104,20 +104,32 @@
 
 initCmd : Model -> Cmd Msg
 initCmd model =
+    let
+        layout =
+            Settings.getLayout model.settings
+    in
     Cmd.batch
         [ Cmd.map DeploymentMsg Deployment.initCmd
         , Cmd.map EditorMsg (Editors.initCmd model.editors)
         , Cmd.map SettingsMsg (Settings.initCmd model.settings)
+        , Cmd.map SplitMsg (SplitPane.initCmd layout)
         ]
 
 
 defaultModel : Settings.Scheme -> Model
 defaultModel scheme =
+    let
+        settings =
+            Settings.init scheme
+
+        layout =
+            Settings.getLayout settings
+    in
     { deployment = Deployment.init
     , share = Share.init
     , editors = Editors.init
-    , split = SplitPane.init SplitPane.Vertical 0.5 splitId
-    , settings = Settings.init scheme
+    , split = SplitPane.init layout 0.5 splitId
+    , settings = settings
     , news = News.init
     , window = Window.init
     }
@@ -178,11 +190,31 @@
                 ( newSettings, settingsCmd ) =
                     Settings.update settingsMsg model.settings
 
+                layout =
+                    Settings.getLayout newSettings
+
+                ( newSplit, splitCmd ) =
+                    SplitPane.update (SplitPane.SetLayout layout) model.split
+
                 newModel =
-                    { model | settings = newSettings }
+                    { model | settings = newSettings, split = newSplit }
+
+                cmds =
+                    if settingsMsg == Settings.ClearDataAndReload then
+                        -- We're about to reload, so avoid other commands which
+                        -- could persist the model after we clear localStorage
+                        -- but before the browser reloads the page.
+                        [ settingsCmd ]
+
+                    else
+                        [ settingsCmd
+                        , Cmd.map SplitMsg splitCmd
+                        , Ports.resizeEditors
+                        , persistModel newModel
+                        ]
             in
             ( newModel
-            , Cmd.batch [ settingsCmd, persistModel newModel ]
+            , Cmd.batch cmds
             )
 
         ShareMsg shareMsg ->
@@ -208,7 +240,7 @@
                 cmds =
                     [ Cmd.map SplitMsg splitCmd
                     , case splitMsg of
-                        SplitPane.ToggleOrientation ->
+                        SplitPane.AutoOrient _ ->
                             Cmd.batch
                                 [ Ports.resizeEditors
                                 , persistModel newModel
@@ -273,10 +305,14 @@
 
 subscriptions : Model -> Sub Msg
 subscriptions model =
+    let
+        layout =
+            Settings.getLayout model.settings
+    in
     Sub.batch
         [ Sub.map DeploymentMsg Deployment.subscriptions
         , Sub.map ShareMsg Share.subscriptions
-        , Sub.map SplitMsg (SplitPane.subscriptions model.split)
+        , Sub.map SplitMsg (SplitPane.subscriptions model.split layout)
         , Sub.map WindowMsg (Window.subscriptions model.window)
         ]
 
@@ -378,9 +414,8 @@
     Html.div
         [ Attributes.class "controls-container" ]
         [ button "Share" (toggle ShareWindow)
-        , button "Help" (toggle HelpWindow)
         , button "Settings" (toggle SettingsWindow)
-        , button "Layout" (SplitMsg SplitPane.ToggleOrientation)
+        , button "Help" (toggle HelpWindow)
         ]
 
 
@@ -485,15 +520,6 @@
 """
                 ]
             , Html.li []
-                [ text "Click the "
-                , bold "Layout"
-                , text " "
-                , multiline """
-button to switch between the left/right editor layout and the top/bottom one
-(useful for long lines and narrow windows).
-"""
-                ]
-            , Html.li []
                 [ text "The app "
                 , bold "autosaves"
                 , text " "
diff --git a/frontend/src/elm/Settings.elm b/frontend/src/elm/Settings.elm
index 5aa75ea..1a0f624 100644
--- a/frontend/src/elm/Settings.elm
+++ b/frontend/src/elm/Settings.elm
@@ -5,12 +5,13 @@
 
 module Settings exposing
     ( Model
-    , Msg
+    , Msg(..)
     , Scheme
     , decode
     , decodeScheme
     , defaultScheme
     , encode
+    , getLayout
     , init
     , initCmd
     , themeClass
@@ -29,6 +30,7 @@
 import Ports
 import Regex
 import Set exposing (Set)
+import SplitPane
 
 
 
@@ -75,6 +77,19 @@
             "tomorrow_night"
 
 
+getLayout : Model -> SplitPane.Layout
+getLayout model =
+    case Form.getStringValue form "layout" model.form of
+        "horizontal" ->
+            SplitPane.Locked SplitPane.Horizontal
+
+        "vertical" ->
+            SplitPane.Locked SplitPane.Vertical
+
+        _ ->
+            SplitPane.Auto
+
+
 getTheme : Model -> String
 getTheme model =
     Form.getStringValue form "theme" model.form
@@ -102,7 +117,8 @@
 form : Form
 form =
     Form.define
-        [ Form.groupedSelect "theme" "Theme" themes "textmate"
+        [ Form.select "layout" "Layout" layouts "auto"
+        , Form.groupedSelect "theme" "Theme" themes "textmate"
         , Form.select "keyboardHandler" "Keybindings" keybindings "ace"
         , Form.number "refreshDelay" "Refresh delay (ms)" 500 Form.positive
         , Form.number "fontSize" "Font size" 14 Form.positive
@@ -135,6 +151,14 @@
         ]
 
 
+layouts : List ( Form.Identifier, String )
+layouts =
+    [ ( "auto", "Auto" )
+    , ( "horizontal", "Horizontal" )
+    , ( "vertical", "Vertical" )
+    ]
+
+
 themes : List ( String, List ( Form.Identifier, String ) )
 themes =
     [ ( "Light"
diff --git a/frontend/src/elm/SplitPane.elm b/frontend/src/elm/SplitPane.elm
index ec73a2b..9c43996 100644
--- a/frontend/src/elm/SplitPane.elm
+++ b/frontend/src/elm/SplitPane.elm
@@ -4,12 +4,14 @@
 
 
 module SplitPane exposing
-    ( Model
+    ( Layout(..)
+    , Model
     , Msg(..)
     , Orientation(..)
     , decode
     , encode
     , init
+    , initCmd
     , subscriptions
     , update
     , view
@@ -59,6 +61,14 @@
     }
 
 
+{-| Layout chosen by the user. Auto means Horizontal for narrow windows and
+Vertical for wide windows.
+-}
+type Layout
+    = Locked Orientation
+    | Auto
+
+
 {-| Orientation can be confusing. This module uses the terms to refer to the
 splitter, so Horizontal means top/bottom and Vertical means left/right.
 -}
@@ -95,20 +105,36 @@
 
 {-| Initializes a split pane. For example:
 
-    init Vertical 0.5 "SplitContainer"
+    init Auto 0.5 "SplitContainer"
 
 To use multiple split containers in a page, they must have unique IDs.
 
 -}
-init : Orientation -> Position -> String -> Model
-init orientation splitPosition id =
-    { orientation = orientation
+init : Layout -> Position -> String -> Model
+init layout splitPosition id =
+    { orientation =
+        case layout of
+            Locked orientation ->
+                orientation
+
+            Auto ->
+                Vertical
     , state = Static splitPosition
     , id = id
     , bounds = emptyBounds
     }
 
 
+initCmd : Layout -> Cmd Msg
+initCmd layout =
+    case layout of
+        Locked _ ->
+            Cmd.none
+
+        Auto ->
+            autoOrient
+
+
 emptyBounds : Bounds
 emptyBounds =
     { x = 0, y = 0, width = 0, height = 0 }
@@ -130,8 +156,10 @@
 
 type Msg
     = NoOp
-      -- Toggles the orientation.
-    | ToggleOrientation
+      -- Update the layout.
+    | SetLayout Layout
+      -- Set the orientation for the Auto layout, given the window width.
+    | AutoOrient Float
       -- PreDragStart is necessary to kick off getting the container div bounds.
       -- Without it, if you resize the window and then start dragging, there is
       -- an annoying flicker effect.
@@ -163,17 +191,25 @@
         NoOp ->
             ( model, Cmd.none )
 
-        ToggleOrientation ->
-            let
-                orientation =
-                    case model.orientation of
-                        Horizontal ->
-                            Vertical
+        SetLayout layout ->
+            case layout of
+                Locked orientation ->
+                    ( { model | orientation = orientation }, Cmd.none )
 
-                        Vertical ->
-                            Horizontal
-            in
-            ( { model | orientation = orientation }, Cmd.none )
+                Auto ->
+                    ( model, autoOrient )
+
+        AutoOrient width ->
+            ( { model
+                | orientation =
+                    if width < 1200 then
+                        Horizontal
+
+                    else
+                        Vertical
+              }
+            , Cmd.none
+            )
 
         PreDragStart ->
             ( model, startDrag model )
@@ -207,6 +243,11 @@
             )
 
 
+autoOrient : Cmd Msg
+autoOrient =
+    Task.perform (\vp -> AutoOrient vp.viewport.width) Browser.Dom.getViewport
+
+
 startDrag : Model -> Cmd Msg
 startDrag model =
     let
@@ -242,19 +283,33 @@
 ------------- SUBSCRIPTIONS ----------------------------------------------------
 
 
-subscriptions : Model -> Sub Msg
-subscriptions model =
-    case model.state of
-        Static _ ->
-            Sub.none
+subscriptions : Model -> Layout -> Sub Msg
+subscriptions model layout =
+    let
+        mouseSubscriptions =
+            case model.state of
+                Static _ ->
+                    []
 
-        Moving _ ->
-            Sub.batch
-                [ Browser.Events.onMouseMove
-                    (Decode.map2 DragMove decodeMouseState decodeCoords)
-                , Browser.Events.onMouseUp
-                    (Decode.map DragStop decodeCoords)
-                ]
+                Moving _ ->
+                    [ Browser.Events.onMouseMove
+                        (Decode.map2 DragMove decodeMouseState decodeCoords)
+                    , Browser.Events.onMouseUp
+                        (Decode.map DragStop decodeCoords)
+                    ]
+
+        handleResize =
+            \w _ -> AutoOrient (toFloat w)
+
+        resizeSubscriptions =
+            case layout of
+                Locked _ ->
+                    []
+
+                Auto ->
+                    [ Browser.Events.onResize handleResize ]
+    in
+    Sub.batch (mouseSubscriptions ++ resizeSubscriptions)
 
 
 decodeMouseState : Decode.Decoder MouseState