blob: 6c515a27f6184992a8ea52be65e779432864d92d [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 Share
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
, share : Share.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
| ShareWindow
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
, share = Share.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
| ShareMsg Share.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 ]
)
ShareMsg shareMsg ->
let
( newShare, shareCmd ) =
Share.update shareMsg model.share
newModel =
{ model | share = newShare }
in
( newModel
, shareCmd
)
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
Window.Show ShareWindow ->
Ports.updateShareLink
_ ->
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 ShareMsg Share.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
]
ShareWindow ->
Html.div []
[ Html.h2 [] [ Html.text "Share" ]
, Share.view model.share
]
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 "Share" (toggle ShareWindow)
, 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 "Use the "
, bold "Version"
, text " "
, multiline """
text field under the FIDL editor to choose versions. For example,
“test:7 fuchsia:HEAD” selects version 7 of platform “test” and the HEAD version
of Fuchsia. See
"""
, text " "
, Html.a [ Attributes.href rfc0083Url ] [ text "RFC-0083" ]
, text " for details."
]
, Html.li []
[ text "Click the "
, bold "Share"
, text " "
, multiline """
button to generate a link you can share with other people. This link saves
everything from the FIDL editor.
"""
]
, 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
]
rfc0083Url : String
rfc0083Url =
"https://fuchsia.dev/fuchsia-src/contribute/governance/rfcs/0083_fidl_versioning"
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.map7 Model
(Decode.succeed Deployment.init)
(Decode.succeed Share.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)