mirror of
https://github.com/tauri-apps/web-view.git
synced 2026-02-04 02:11:18 +01:00
Add elm example
This commit is contained in:
@@ -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
109
examples/todo-elm.rs
Normal 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
1
examples/todo-elm/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
elm-stuff/
|
||||
4
examples/todo-elm/app.js
Normal file
4
examples/todo-elm/app.js
Normal 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
5477
examples/todo-elm/elm.js
Normal file
File diff suppressed because it is too large
Load Diff
24
examples/todo-elm/elm.json
Normal file
24
examples/todo-elm/elm.json
Normal 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": {}
|
||||
}
|
||||
}
|
||||
57
examples/todo-elm/index.html
Normal file
57
examples/todo-elm/index.html
Normal 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>
|
||||
170
examples/todo-elm/src/Main.elm
Normal file
170
examples/todo-elm/src/Main.elm
Normal 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
|
||||
100
examples/todo-elm/styles.css
Normal file
100
examples/todo-elm/styles.css
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user