blob: c7cd869b3e7b23fc2a7331f1abb85324d2abb8b9 [file] [log] [blame]
-- 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 Main exposing (main)
{-| Frontend SPA for fidlbolt.
-}
import Browser
import Deployment
import Editors
import Html exposing (Html)
import Html.Attributes as Attributes
import Html.Events as Events
import Html.Lazy as Lazy
import Json.Decode as Decode
import Json.Encode as Encode
import News
import Ports
import Settings
import SplitPane
import Window
------------- MAIN -------------------------------------------------------------
main : Program Decode.Value Model Msg
main =
Browser.element
{ init = init
, update = update
, subscriptions = subscriptions
, view = view
}
------------- MODEL ------------------------------------------------------------
type alias Model =
{ deployment : Deployment.Model
, editors : Editors.Model
, settings : Settings.Model
, split : SplitPane.Model
, news : News.Model
, window : Window.Model Window
}
type alias Flags =
{ model : Maybe Model
, preferredScheme : Settings.Scheme
}
type Window
= HelpWindow
| SettingsWindow
init : Decode.Value -> ( Model, Cmd Msg )
init savedValue =
let
model =
case savedValue |> Decode.decodeValue decodeFlags of
Err _ ->
-- Failed to decode flags passed from JS. Use defaults.
defaultModel Settings.defaultScheme
Ok flags ->
case flags.model of
Nothing ->
-- There is no saved model (brand new session). Use
-- the defaults, respecting the user's preferred
-- light/dark color scheme.
defaultModel flags.preferredScheme
Just savedModel ->
-- Use the saved model. Only update the preferred
-- color scheme, which might have changed since the
-- last time (e.g. macOS auto dark mode).
let
savedSettings =
savedModel.settings
newSettings =
{ savedSettings
| preferredScheme =
flags.preferredScheme
}
in
{ savedModel | settings = newSettings }
in
( model, initCmd model )
initCmd : Model -> Cmd Msg
initCmd model =
Cmd.batch
[ Cmd.map DeploymentMsg Deployment.initCmd
, Cmd.map EditorMsg (Editors.initCmd model.editors)
, Cmd.map SettingsMsg (Settings.initCmd model.settings)
]
defaultModel : Settings.Scheme -> Model
defaultModel scheme =
{ deployment = Deployment.init
, editors = Editors.init
, split = SplitPane.init SplitPane.Vertical 0.5 splitId
, settings = Settings.init scheme
, news = News.init
, window = Window.init
}
splitId : String
splitId =
"SplitContainer"
------------- UPDATE -----------------------------------------------------------
type Msg
= DeploymentMsg Deployment.Msg
| EditorMsg Editors.Msg
| SettingsMsg Settings.Msg
| SplitMsg SplitPane.Msg
| NewsMsg News.Msg
| WindowMsg (Window.Msg Window)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
DeploymentMsg deploymentMsg ->
let
newDeployment =
Deployment.update deploymentMsg model.deployment
newModel =
{ model | deployment = newDeployment }
in
( newModel, Cmd.none )
EditorMsg editorMsg ->
let
( newEditors, editorCmd ) =
Editors.update editorMsg model.editors
newModel =
{ model | editors = newEditors }
in
( newModel
, Cmd.batch
[ editorCmd
-- Resize editors in case the options bar appeared/disappeared.
, Ports.resizeEditors
, persistModel newModel
]
)
SettingsMsg settingsMsg ->
let
( newSettings, settingsCmd ) =
Settings.update settingsMsg model.settings
newModel =
{ model | settings = newSettings }
in
( newModel
, Cmd.batch [ settingsCmd, persistModel newModel ]
)
SplitMsg splitMsg ->
let
( newSplit, splitCmd ) =
SplitPane.update splitMsg model.split
newModel =
{ model | split = newSplit }
cmds =
[ Cmd.map SplitMsg splitCmd
, case splitMsg of
SplitPane.ToggleOrientation ->
Cmd.batch
[ Ports.resizeEditors
, persistModel newModel
]
SplitPane.DragMove _ _ ->
Ports.resizeEditors
SplitPane.DragStop _ ->
Cmd.batch
[ Ports.resizeEditors
, persistModel newModel
]
_ ->
Cmd.none
]
in
( newModel, Cmd.batch cmds )
NewsMsg newsMsg ->
let
newModel =
{ model | news = News.update newsMsg model.news }
in
( newModel, persistModel newModel )
WindowMsg windowMsg ->
let
newWindow =
Window.update windowMsg model.window
newModel =
{ model | window = newWindow }
cmds =
[ Ports.setMainTabEnabled (Window.isAbsent newWindow)
, case windowMsg of
Window.Show HelpWindow ->
Cmd.map DeploymentMsg Deployment.checkTime
_ ->
Cmd.none
]
in
( newModel
, Cmd.batch cmds
)
persistModel : Model -> Cmd msg
persistModel model =
Ports.persistModel (encode model)
------------- SUBSCRIPTIONS ----------------------------------------------------
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.batch
[ Sub.map DeploymentMsg Deployment.subscriptions
, Sub.map SplitMsg (SplitPane.subscriptions model.split)
, Sub.map WindowMsg (Window.subscriptions model.window)
]
------------- VIEW -------------------------------------------------------------
view : Model -> Html Msg
view model =
let
modalWindow =
Window.view (windowView model) WindowMsg model.window
themeClass =
Settings.themeClass model.settings
inputEditor =
Lazy.lazy2 (Editors.view Editors.Input EditorMsg)
themeClass
model.editors
outputEditor =
Lazy.lazy2 (Editors.view Editors.Output EditorMsg)
themeClass
model.editors
splitPane =
SplitPane.view inputEditor outputEditor SplitMsg model.split
in
Html.div []
[ modalWindow
, Html.div
[ Attributes.id "Main" ]
[ controlsView model
, Html.div
[ Attributes.class "wrapper" ]
[ headingView
, News.view NewsMsg model.news
, Html.div
[ Attributes.class "container" ]
[ splitPane ]
]
]
]
windowView : Model -> Window -> Html Msg
windowView model window =
case window of
HelpWindow ->
Html.div []
[ Html.h2 [] [ Html.text "Help" ]
, helpMessage model.deployment model.split.orientation
]
SettingsWindow ->
Html.div []
[ Html.h2 [] [ Html.text "Settings" ]
, Settings.view SettingsMsg model.settings
]
headingView : Html Msg
headingView =
Html.div
[ Attributes.class "heading" ]
[ Html.h1 []
[ Html.span [ Attributes.class "heading-fidl" ] [ Html.text "fidl" ]
, Html.span [ Attributes.class "heading-bolt" ] [ Html.text "bolt" ]
]
]
controlsView : Model -> Html Msg
controlsView model =
let
button title msg =
Html.button
[ Attributes.class "button controls-button"
, Attributes.type_ "button"
, Events.onClick msg
]
[ Html.text title ]
toggle window =
if Window.isAbsent model.window then
WindowMsg (Window.Show window)
else
WindowMsg Window.Hide
in
Html.div
[ Attributes.class "controls-container" ]
[ button "Help" (toggle HelpWindow)
, button "Settings" (toggle SettingsWindow)
, button "Layout" (SplitMsg SplitPane.ToggleOrientation)
]
helpMessage : Deployment.Model -> SplitPane.Orientation -> Html msg
helpMessage deployment orientation =
let
text =
Html.text
multiline =
Html.text << String.trim
bold content =
Html.b [] [ text content ]
kbd keys =
Html.span
[ Attributes.class "kbd-group" ]
(List.map (\key -> Html.kbd [] [ text key ]) keys)
( inputLocation, outputLocation ) =
case orientation of
SplitPane.Horizontal ->
( "on top", "below" )
SplitPane.Vertical ->
( "on the left", "on the right" )
in
Html.div []
[ Html.p []
[ text "Welcome to "
, bold "fidlbolt"
, text ", a web app for exploring FIDL code and bytes."
]
, Html.p []
[ text "Here are some tips to help you get started:" ]
, Html.ul []
[ Html.li []
[ text "The "
, bold "input"
, text (" editor is " ++ inputLocation ++ ". ")
, text "Use the tab bar, or "
, kbd [ "Ctrl", "[" ]
, text " and "
, kbd [ "Ctrl", "]" ]
, multiline """
, to choose what kind of input you have. Then, type away in the editor.
"""
]
, Html.li []
[ text "The "
, bold "output"
, text (" view is " ++ outputLocation ++ ". ")
, multiline """
Use the tab bar to choose what kind of output you want. For example, from FIDL
input you can convert to FIDL (which just formats it), or to HLCPP to generate
C++ bindings. Some outputs also have options along the bottom.
"""
]
, Html.li []
[ text "To "
, bold "refresh"
, text " the output, press "
, kbd [ "Ctrl", "Enter" ]
, text " or (on macOS) "
, kbd [ "Cmd", "Enter" ]
, text ". "
, multiline """
Alternatively, just wait a few moments — the output will automatically refresh
after you stop typing.
"""
]
, Html.li []
[ text "Click the "
, bold "Settings"
, text " "
, multiline """
button to configure things. For example, you can try out different color themes,
or use keybindings from your favorite editor.
"""
]
, 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 " "
, multiline """
everything to your browser’s local storage. You can always start fresh by
clicking “Reset all data” in Settings.
"""
]
, Html.li []
[ text "To "
, bold "close"
, text " this help window, press "
, kbd [ "Esc" ]
, text " or "
, kbd [ "Enter" ]
, text "."
]
]
, Html.p []
[ text "Enjoy! If you find a bug, please "
, Html.a [ Attributes.href bugReportUrl ]
[ text "report it here" ]
, text "."
]
, Html.hr [] []
, Deployment.view deployment
]
bugReportUrl : String
bugReportUrl =
"https://bugs.fuchsia.dev/p/fuchsia/issues/entry?components=FIDL%3EFidlbolt"
------------- ENCODE / DECODE --------------------------------------------------
encode : Model -> Encode.Value
encode model =
Encode.object
[ ( "editors", Editors.encode model.editors )
, ( "settings", Settings.encode model.settings )
, ( "split", SplitPane.encode model.split )
, ( "news", News.encode model.news )
]
decode : Decode.Decoder Model
decode =
Decode.map6 Model
(Decode.succeed Deployment.init)
(Decode.field "editors" Editors.decode)
(Decode.field "settings" Settings.decode)
(Decode.field "split" (SplitPane.decode splitId))
(Decode.field "news" News.decode)
(Decode.succeed Window.init)
decodeFlags : Decode.Decoder Flags
decodeFlags =
Decode.map2 Flags
(Decode.field "model" (Decode.maybe decode))
(Decode.field "preferredScheme" Settings.decodeScheme)