Add elm example

This commit is contained in:
zxey
2019-11-02 21:49:52 +01:00
parent 788661593a
commit 7df7aaaac3
9 changed files with 5958 additions and 0 deletions

View File

@@ -49,6 +49,22 @@ In order for this to run on EdgeHTML, you need to run `CheckNetIsolation.exe Loo
You can make this step for example as a part of your apps installer.
## todo-elm
(This assumes you're using Elm 0.19.0).
This example is functionally equivalent to `todo` and `todo-purescript` examples, but implemented in Elm.
It showcases how to communicate from Elm to Rust and back through Elm's ports.
You can run this example as is with `cargo run --example todo-elm`.
If you want to edit the example's sources, you will first need to install Elm as described [here](https://guide.elm-lang.org/install/elm.html).
Then run:
```
elm make --optimize --output=elm.js src/Main.elm
cargo run --example todo-elm
```
The `--output=elm.js` parameter is very important, otherwise `elm make` would output `index.html`.
We include `elm.js` and js glue code (for Elm's ports) in `todo-elm.rs`, so we cannot use `index.html`.
---
Note: For some reason (at least on Windows), if I try to `cargo run` the examples directly, they don't show the window, but it works with `cargo build --example <name> && target\debug\examples\<name>`

109
examples/todo-elm.rs Normal file
View File

@@ -0,0 +1,109 @@
//#![windows_subsystem = "windows"]
#[macro_use]
extern crate serde_derive;
extern crate serde_json;
extern crate web_view;
use web_view::*;
fn main() {
let html = format!(r#"<!doctype html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta charset="UTF-8">
{styles}
</head>
<body>
<!--[if lt IE 11]>
<div class="ie-upgrade-container">
<p class="ie-upgrade-message">Please, upgrade Internet Explorer to continue using this software.</p>
<a class="ie-upgrade-link" target="_blank" href="https://www.microsoft.com/en-us/download/internet-explorer.aspx">Upgrade</a>
</div>
<![endif]-->
<div id="elm"></div>
{scripts}
</body>
</html>
"#,
styles = inline_style(include_str!("todo-elm/styles.css")),
scripts = inline_script(include_str!("todo-elm/elm.js")) + &inline_script(include_str!("todo-elm/app.js")),
);
let mut webview = web_view::builder()
.title("Rust Todo App")
.content(Content::Html(html))
.size(320, 480)
.resizable(false)
.debug(true)
.user_data(vec![])
.invoke_handler(|webview, arg| {
use Cmd::*;
let tasks_len = {
let tasks = webview.user_data_mut();
match serde_json::from_str(arg).unwrap() {
Init => {
*tasks = vec![
Task {
name: "Create Elm example".to_string(),
done: true,
},
];
},
Log { text } => println!("{}", text),
AddTask { name } => tasks.push(Task { name, done: false }),
MarkTask { index, done } => tasks[index].done = done,
ClearDoneTasks => tasks.retain(|t| !t.done),
}
tasks.len()
};
webview.set_title(&format!("Rust Todo App ({} Tasks)", tasks_len))?;
render(webview)
})
.build()
.unwrap();
webview.set_color((156, 39, 176));
let res = webview.run().unwrap();
println!("final state: {:?}", res);
}
fn render(webview: &mut WebView<Vec<Task>>) -> WVResult {
let render_tasks = {
let tasks = webview.user_data();
println!("{:#?}", tasks);
format!("app.ports.fromRust.send({})", serde_json::to_string(tasks).unwrap())
};
webview.eval(&render_tasks)
}
#[derive(Debug, Serialize, Deserialize)]
struct Task {
name: String,
done: bool,
}
#[derive(Deserialize)]
#[serde(tag = "cmd")]
pub enum Cmd {
Init,
Log { text: String },
AddTask { name: String },
MarkTask { index: usize, done: bool },
ClearDoneTasks,
}
fn inline_style(s: &str) -> String {
format!(r#"<style type="text/css">{}</style>"#, s)
}
fn inline_script(s: &str) -> String {
format!(r#"<script type="text/javascript">{}</script>"#, s)
}

1
examples/todo-elm/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
elm-stuff/

4
examples/todo-elm/app.js Normal file
View File

@@ -0,0 +1,4 @@
var app = Elm.Main.init({ node: document.getElementById("elm") });
app.ports.toRust.subscribe(function(data) {
window.external.invoke(JSON.stringify(data));
});

5477
examples/todo-elm/elm.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,24 @@
{
"type": "application",
"source-directories": [
"src"
],
"elm-version": "0.19.1",
"dependencies": {
"direct": {
"elm/browser": "1.0.1",
"elm/core": "1.0.2",
"elm/html": "1.0.0",
"elm/json": "1.1.3"
},
"indirect": {
"elm/time": "1.0.0",
"elm/url": "1.0.0",
"elm/virtual-dom": "1.0.2"
}
},
"test-dependencies": {
"direct": {},
"indirect": {}
}
}

View File

@@ -0,0 +1,57 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Elm - Todo</title>
<script type="text/javascript" src="elm.js"></script>
<link rel="stylesheet" href="styles.css">
</head>
<!--
This index.html is here only for debugging purposes,
it is not used in todo-elm Rust example.
This is the same backend as it is in todo-elm.rs but implemented
in JavaScript, you can open this in your browser directly and
it should work without Rust.
-->
<body>
<div id="elm">
</div>
<script type="text/javascript">
var tasks = [];
var app = Elm.Main.init({ node: document.getElementById("elm") });
app.ports.toRust.subscribe(function (data) {
console.debug(data);
switch (data.cmd) {
case "Init":
tasks = [{ name: "Create elm example", done: true }];
break;
case "Log":
console.log(data.text);
break;
case "AddTask":
tasks.push({ name: data.name, done: false });
break;
case "MarkTask":
// we use filter instead of find for compatibility with IE11
var task = tasks.filter(function (_, index) { return index == data.index })[0];
if (task) {
task.done = data.done;
}
break;
case "ClearDoneTasks":
tasks = tasks.filter(function (value) { return !value.done; });
break;
}
if (data.cmd != "Log") {
app.ports.fromRust.send(tasks);
}
})
</script>
</body>
</html>

View File

@@ -0,0 +1,170 @@
port module Main exposing (..)
import Browser
import Html exposing (Html, button, div, form, input, li, text, ul)
import Html.Attributes exposing (autofocus, class, classList, id, type_, value)
import Html.Events exposing (onClick, onInput, onSubmit)
import Json.Decode as Decode exposing (Decoder, field, map2)
import Json.Encode exposing (Value, bool, encode, int, object, string)
port toRust : Value -> Cmd msg
port fromRust : (Value -> msg) -> Sub msg
main =
Browser.element
{ init = init
, update = update
, view = view
, subscriptions = subscriptions
}
type RustCommand
= Init
| Log { text : String }
| AddTask { name : String }
| MarkTask { index : Int, done : Bool }
| ClearDoneTasks
encodeRustCommand : RustCommand -> Value
encodeRustCommand command =
case command of
Init ->
object [ ( "cmd", string "Init" ) ]
Log { text } ->
object [ ( "cmd", string "Log" ), ( "text", string text ) ]
AddTask { name } ->
object [ ( "cmd", string "AddTask" ), ( "name", string name ) ]
MarkTask { index, done } ->
object [ ( "cmd", string "MarkTask" ), ( "index", int index ), ( "done", bool done ) ]
ClearDoneTasks ->
object [ ( "cmd", string "ClearDoneTasks" ) ]
-- MODEL
type alias Task =
{ name : String
, done : Bool
}
type alias Model =
{ str : String
, field : String
, tasks : List Task
}
type Msg
= UpdateField String
| SendToRust RustCommand
| UpdateTasks (List Task)
init : () -> ( Model, Cmd Msg )
init _ =
( { str = "", field = "", tasks = [] }, toRust (encodeRustCommand Init) )
----- UPDATE
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
UpdateTasks tasks ->
( { model | tasks = tasks }, Cmd.none )
SendToRust command ->
( model, toRust (encodeRustCommand command) )
UpdateField field ->
( { model | field = field }, Cmd.none )
-- VIEW
viewTask : Int -> Task -> Html Msg
viewTask index task =
div
[ classList
[ ( "task-item", True )
, ( "checked", task.done == True )
, ( "unchecked", task.done == False )
]
, onClick (SendToRust (MarkTask { index = index, done = not task.done }))
]
[ text task.name ]
view : Model -> Html Msg
view model =
div [ class "container" ]
[ text model.str
, form
[ class "text-input-wrapper", onSubmit (SendToRust (AddTask { name = model.field })) ]
[ input
[ id "task-name-input"
, class "text-input"
, type_ "text"
, autofocus True
, value model.field
, onInput UpdateField
]
[]
]
, div [ class "task-list" ] (List.indexedMap viewTask model.tasks)
, div [ class "footer" ]
[ div [ class "btn-clear-tasks", onClick (SendToRust ClearDoneTasks) ] [ text "Delete completed" ]
]
]
-- SUBSCRIPTIONS
taskDecoder : Decoder Task
taskDecoder =
map2 Task
(field "name" Decode.string)
(field "done" Decode.bool)
taskListDecoder : Decoder (List Task)
taskListDecoder =
Decode.list taskDecoder
decodeValue : Value -> Msg
decodeValue x =
let
result =
Decode.decodeValue taskListDecoder x
in
case result of
Ok tasks ->
UpdateTasks tasks
Err err ->
SendToRust (Log { text = Decode.errorToString err })
subscriptions : Model -> Sub Msg
subscriptions model =
fromRust decodeValue

View File

@@ -0,0 +1,100 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-size: 28px;
font-family: sans-serif;
}
html, body {
height: 100%;
overflow: none;
}
.ie-upgrade-container {
width: 100%;
height: 100%;
font-family: Arial, sans-serif;
font-size: 32px;
color: #ffffff;
background-color: #1ebbee;
padding: 3em 1em 1em 1em;
}
.ie-upgrade-link {
margin: 2em 0em;
padding: 0 1em;
color: #1ebbee;
background-color: #ffffff;
font-weight: bold;
text-align: center;
display: block;
width: 100%;
height: 2em;
line-height: 2em;
text-transform: uppercase;
}
.container {
width: 100%;
height: 100%;
background-color: #9c27b0;
}
.text-input-wrapper {
padding: 0.5em;
position: fixed;
top: 0;
left: 0;
righ: 0;
}
.text-input {
width: 100%;
line-height: 1.5em;
padding: 0 0.2em;
height: 1.5em;
outline: none;
border: none;
color: #4a148c;
background-color: rgba(255, 255, 255, 0.87);
}
.text-input:focus {
background-color: #ffffff;
}
.task-list {
overflow-y: auto;
position: fixed;
top: 2.5em;
bottom: 1.2em;
left: 0;
right: 0;
}
.task-item {
height: 1.5em;
color: rgba(255, 255, 255, 0.87);
padding: 0.5em;
cursor: pointer;
}
.task-item.checked {
text-decoration: line-through;
color: rgba(255, 255, 255, 0.38);
}
.footer {
position: fixed;
left: 0;
bottom: 0;
right: 0;
background-color: #ffffff;
color: #9c27b0;
}
.btn-clear-tasks {
width: 100%;
text-align: center;
font-size: 18px;
height: 2.5em;
line-height: 2.5em;
text-transform: uppercase;
cursor: pointer;
}