├── .gitignore ├── .gitmodules ├── .travis.yml ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── README.md ├── minimal.rs ├── pageload.rs ├── timer.rs ├── todo-ps │ ├── .gitignore │ ├── dist │ │ └── bundle.html │ ├── entry.js │ ├── index.html │ ├── package.json │ ├── psc-package.json │ ├── src │ │ ├── Component │ │ │ └── Todo.purs │ │ └── Main.purs │ └── styles.css ├── todo-purescript.rs ├── todo.rs └── todo │ ├── app.js │ ├── picodom.js │ └── styles.css ├── src └── lib.rs └── webview-sys ├── Cargo.toml ├── build.rs ├── lib.rs └── webview.c /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | target/ 3 | **/*.rs.bk 4 | Cargo.lock 5 | TODO.md 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "webview-sys/webview"] 2 | path = webview-sys/webview 3 | url = https://github.com/huytd/webview 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: rust 3 | cache: cargo 4 | 5 | os: 6 | - linux 7 | - osx 8 | 9 | rust: 10 | - stable 11 | - beta 12 | - nightly 13 | 14 | matrix: 15 | allow_failures: 16 | - rust: nightly 17 | 18 | addons: 19 | apt: 20 | packages: 21 | - libwebkit2gtk-4.0-dev 22 | sources: 23 | - sourceline: 'ppa:webkit-team/ppa' 24 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "web-view" 3 | version = "0.1.3" 4 | authors = ["Boscop"] 5 | readme = "README.md" 6 | license = "MIT" 7 | repository = "https://github.com/Boscop/web-view" 8 | description = "Rust bindings for webview, a tiny cross-platform library to render web-based GUIs for desktop applications" 9 | keywords = ["web", "gui", "desktop", "electron", "webkit"] 10 | categories = ["gui", "web-programming", "api-bindings", "rendering", "visualization"] 11 | exclude = ["examples/todo-ps/dist/**/*"] 12 | 13 | [dependencies] 14 | urlencoding = "1.0" 15 | fnv = "1.0" 16 | webview-sys = { path = "webview-sys", version = "0.1.0" } 17 | 18 | [dev-dependencies] 19 | serde = "1.0" 20 | serde_derive = "1.0" 21 | serde_json = "1.0" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Boscop 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # web-view   [![Build Status]][travis] [![Latest Version]][crates.io] 2 | 3 | [Build Status]: https://api.travis-ci.org/Boscop/web-view.svg?branch=master 4 | [travis]: https://travis-ci.org/Boscop/web-view 5 | [Latest Version]: https://img.shields.io/crates/v/web-view.svg 6 | [crates.io]: https://crates.io/crates/web-view 7 | 8 | This library provides Rust bindings for the [webview](https://github.com/zserge/webview) library to allow easy creation of cross-platform Rust desktop apps with GUIs based on web technologies. 9 | 10 | It supports two-way bindings for communication between the Rust backend and JavaScript frontend. 11 | 12 | It uses Cocoa/WebKit on macOS, gtk-webkit2 on Linux and MSHTML (IE10/11) on Windows, so your app will be **much** leaner than with Electron. 13 | 14 | For usage info please check out [the examples](../../tree/master/examples) and the [original readme](https://github.com/zserge/webview/blob/master/README.md). 15 | 16 | Contributions and feedback welcome :) 17 | 18 |

screenshot

19 | 20 | ## Suggestions: 21 | - If you like type safety, write your frontend in [Elm](http://elm-lang.org/) or [PureScript](http://www.purescript.org/)[*](#n1), or use a Rust frontend framework that compiles to asm.js, like [yew](https://github.com/DenisKolodin/yew). 22 | - Use [parcel](https://parceljs.org/) to bundle & minify your frontend code. 23 | - Use [inline-assets](https://www.npmjs.com/package/inline-assets) to inline all your assets (css, js, html) into one index.html file and embed it in your Rust app using `include_str!()`. 24 | - If your app runs on windows, [add an icon](https://github.com/mxre/winres) to your Rust executable to make it look more professional™ 25 | - Use custom npm scripts or [just](https://github.com/casey/just) or [cargo-make](https://github.com/sagiegurari/cargo-make) to automate the build steps. 26 | - Make your app state persistent between sessions using localStorage in the frontend or [rustbreak](https://crates.io/crates/rustbreak) in the backend. 27 | - Btw, instead of injecting app resources via the js api, you can also serve them from a local http server (e.g. bound to an ephemeral port). 28 | - Happy coding :) 29 | 30 | * The free [PureScript By Example](https://leanpub.com/purescript/read) book contains several practical projects for PureScript beginners. 31 | 32 | --- 33 | 34 | ## Contribution opportunities: 35 | - Create an issue for any question you have 36 | - Docs 37 | - Feedback on this library's API and code 38 | - Some functions don't have FFI yet, e.g. `webview_dialog` 39 | - Test it on non-windows platforms, report any issues you find 40 | - Showcase your app 41 | - Add an example that uses Elm or Rust compiled to asm.js 42 | - Add a PureScript example that does two-way communication with the backend 43 | - Contribute to the original webview library: E.g. [add HDPI support on Windows](https://github.com/zserge/webview/issues/54) 44 | - Make it possible to create the webview window as a child window of a given parent window. This would allow webview to be used for the GUIs of [VST audio plugins in Rust](https://github.com/rust-dsp/rust-vst). 45 | 46 | --- 47 | 48 | ### Ideas for apps: 49 | - Rust IDE (by porting [xi-electron](https://github.com/acheronfail/xi-electron) to web-view) 50 | - Data visualization / plotting lib for Rust, to make Rust more useful for data science 51 | - Crypto coin wallet 52 | - IRC client, or client for other chat protocols 53 | - Midi song editor, VJ controller 54 | - Rust project template wizard: Generate new Rust projects from templates with user-friendly steps 55 | - GUI for [pijul](https://pijul.org/) 56 | 57 | --- 58 | 59 | Note: The API can still change. Currently there is only one function (`run()`) that takes all args, but maybe a builder would be better.. 60 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # web-view examples 2 | 3 | ## minimal 4 | Just displays the wikipedia homepage. 5 | 6 | ## pageload 7 | Loads a custom url-encoded html page (hello world). 8 | 9 | ## timer 10 | Uses two-way communication with the web app to render the state of a timer and reset the timer on the click of a button. Shows basic usage of `userdata` and shared state between threads. 11 | 12 | ## todo 13 | Uses picodom.js to render a basic Todo App. Demonstrates how to embed the frontend into the Rust executable and how to use `userdata` to store app state. 14 | 15 | ## todo-purescript 16 | This is a port of the todo example to PureScript. 17 | To be able to build this, first install purescript and bundling tools: 18 | ``` 19 | $ npm install -g purescript pulp psc-package parcel-bundler inline-assets 20 | ``` 21 | Next, install the dependencies: 22 | ``` 23 | $ psc-package update 24 | ``` 25 | Now build the frontend and bundle it into `dist/bundle.html`: 26 | ``` 27 | $ npm run prod 28 | ``` 29 | Finally use cargo to build the rust executable, which includes `bundle.html` using `include_str!()`. 30 | 31 | --- 32 | 33 | 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 && target\debug\examples\` 34 | -------------------------------------------------------------------------------- /examples/minimal.rs: -------------------------------------------------------------------------------- 1 | // #![windows_subsystem = "windows"] 2 | 3 | extern crate web_view; 4 | 5 | use web_view::*; 6 | 7 | fn main() { 8 | let size = (800, 600); 9 | let resizable = true; 10 | let debug = true; 11 | let init_cb = |_webview| {}; 12 | let frontend_cb = |_webview: &mut _, _arg: &_, _userdata: &mut _| {}; 13 | let userdata = (); 14 | run( 15 | "Minimal webview example", 16 | Content::Url("https://en.m.wikipedia.org/wiki/Main_Page"), 17 | Some(size), 18 | resizable, 19 | debug, 20 | init_cb, 21 | frontend_cb, 22 | userdata 23 | ); 24 | } -------------------------------------------------------------------------------- /examples/pageload.rs: -------------------------------------------------------------------------------- 1 | // #![windows_subsystem = "windows"] 2 | 3 | extern crate web_view; 4 | 5 | use web_view::*; 6 | 7 | fn main() { 8 | let size = (800, 600); 9 | let resizable = true; 10 | let debug = true; 11 | let init_cb = |_webview| {}; 12 | let frontend_cb = |_webview: &mut _, _arg: &_, _userdata: &mut _| {}; 13 | let userdata = (); 14 | run("pageload example", Content::Html(HTML), Some(size), resizable, debug, init_cb, frontend_cb, userdata); 15 | } 16 | 17 | const HTML: &'static str = r#" 18 | 19 | 20 | 21 |

Hello, world

22 | 23 | 24 | "#; -------------------------------------------------------------------------------- /examples/timer.rs: -------------------------------------------------------------------------------- 1 | // #![windows_subsystem = "windows"] 2 | #![allow(deprecated)] 3 | 4 | extern crate web_view; 5 | 6 | use std::thread::{spawn, sleep_ms}; 7 | use std::sync::{Arc, Mutex}; 8 | use web_view::*; 9 | 10 | fn main() { 11 | let size = (800, 600); 12 | let resizable = true; 13 | let debug = true; 14 | let initial_userdata = 0; 15 | let counter = Arc::new(Mutex::new(0)); 16 | let counter_inner = counter.clone(); 17 | run("timer example", Content::Html(HTML), Some(size), resizable, debug, move |webview| { 18 | spawn(move || { 19 | loop { 20 | { 21 | let mut counter = counter_inner.lock().unwrap(); 22 | *counter += 1; 23 | webview.dispatch(|webview, userdata| { 24 | *userdata -= 1; 25 | render(webview, *counter, *userdata); 26 | }); 27 | } 28 | sleep_ms(1000); 29 | } 30 | }); 31 | }, move |webview, arg, userdata| { 32 | match arg { 33 | "reset" => { 34 | *userdata += 10; 35 | let mut counter = counter.lock().unwrap(); 36 | *counter = 0; 37 | render(webview, *counter, *userdata); 38 | } 39 | "exit" => { 40 | webview.terminate(); 41 | } 42 | _ => unimplemented!() 43 | } 44 | }, initial_userdata); 45 | } 46 | 47 | fn render<'a, T>(webview: &mut WebView<'a, T>, counter: u32, userdata: i32) { 48 | println!("counter: {}, userdata: {}", counter, userdata); 49 | webview.eval(&format!("updateTicks({}, {})", counter, userdata)); 50 | } 51 | 52 | const HTML: &'static str = r#" 53 | 54 | 55 | 56 |

57 | 58 | 59 | 64 | 65 | 66 | "#; -------------------------------------------------------------------------------- /examples/todo-ps/.gitignore: -------------------------------------------------------------------------------- 1 | /bower_components/ 2 | /node_modules/ 3 | /.pulp-cache/ 4 | /output/ 5 | /generated-docs/ 6 | /.psc-package/ 7 | /.psc* 8 | /.purs* 9 | /.psa* 10 | .cache 11 | /dist/* 12 | !/dist/bundle.html 13 | /build/ 14 | -------------------------------------------------------------------------------- /examples/todo-ps/dist/bundle.html: -------------------------------------------------------------------------------- 1 | Rust / PureScript - Todo App -------------------------------------------------------------------------------- /examples/todo-ps/entry.js: -------------------------------------------------------------------------------- 1 | require("./output/Main").main(); 2 | -------------------------------------------------------------------------------- /examples/todo-ps/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Rust / PureScript - Todo App 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/todo-ps/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "pulp build --to dist/app.js", 4 | "watch": "pulp -w build --to dist/app.js", 5 | "watch-output": "pulp -w build", 6 | "watch-fast": "webpack --entry ./entry.js --output-filename dist/app.js --progress --watch", 7 | "reload": "browser-sync start --server --files \"dist/**/*.*, index.html, *.css\" --no-ghost-mode --port 2345 --no-open", 8 | "dev-slow": "npm-run-all -p -r watch reload", 9 | "dev": "npm-run-all -p -r watch-output watch-fast reload", 10 | "prod": "pulp browserify -O --to build/app.js && parcel build build/app.js && inline-assets --htmlmin --cssmin index.html dist/bundle.html", 11 | "test": "pulp test" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/todo-ps/psc-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todo-ps", 3 | "set": "psc-0.11.7", 4 | "source": "https://github.com/purescript/package-sets.git", 5 | "depends": [ 6 | "halogen", 7 | "prelude" 8 | ] 9 | } -------------------------------------------------------------------------------- /examples/todo-ps/src/Component/Todo.purs: -------------------------------------------------------------------------------- 1 | module Component.Todo where 2 | 3 | import Prelude 4 | 5 | import Data.Maybe (Maybe(..), fromMaybe) 6 | import Data.Array (snoc, filter, length, mapWithIndex, modifyAt) 7 | 8 | import Halogen as H 9 | import Halogen.HTML as HH 10 | import Halogen.HTML.Events as HE 11 | import Halogen.HTML.Properties as HP 12 | 13 | import DOM.HTML.Indexed.InputType as IT 14 | import DOM.Event.KeyboardEvent (key) 15 | 16 | type Task = 17 | { name :: String 18 | , done :: Boolean 19 | } 20 | 21 | type State = 22 | { tasks :: Array Task 23 | , numCompleted :: Int 24 | , newTaskName :: String 25 | } 26 | 27 | initialState :: State 28 | initialState = 29 | { tasks: [] 30 | , numCompleted: 0 31 | , newTaskName: "" 32 | } 33 | 34 | data Query a 35 | = UpdateNewTask String a 36 | | NewTask a 37 | | ToggleCompleted Int a 38 | | RemoveCompleted a 39 | 40 | type Message = Void 41 | 42 | component :: forall eff. H.Component HH.HTML Query Unit Message eff 43 | component = 44 | H.component 45 | { initialState: const initialState 46 | , render 47 | , eval 48 | , receiver: const Nothing 49 | } 50 | where 51 | 52 | render :: State -> H.ComponentHTML Query 53 | render st = 54 | HH.div [ HP.class_ $ H.ClassName "container" ] 55 | [ HH.div [ HP.class_ $ H.ClassName "text-input-wrapper" ] 56 | [ HH.input 57 | [ HP.type_ IT.InputText 58 | , HP.class_ $ H.ClassName "text-input" 59 | , HP.autofocus true 60 | , HP.placeholder "new task" 61 | , HP.value st.newTaskName 62 | , HE.onValueInput (HE.input UpdateNewTask) 63 | , HE.onKeyDown \e -> case key e of 64 | "Enter" -> Just (H.action NewTask) 65 | _ -> Nothing 66 | ] 67 | ] 68 | , HH.div [ HP.class_ $ H.ClassName "task-list" ] $ mapWithIndex renderTask st.tasks 69 | , HH.div [ HP.class_ $ H.ClassName "footer" ] 70 | [ HH.div 71 | [ HP.class_ $ H.ClassName "btn-clear-tasks" 72 | , HE.onClick (HE.input_ RemoveCompleted) 73 | ] 74 | [ HH.text $ "Delete completed (" <> show st.numCompleted <> "/" <> show (length st.tasks) <> ")" ] 75 | ] 76 | ] 77 | 78 | renderTask i t = 79 | HH.div 80 | [ HP.class_ $ H.ClassName $ "task-item " <> checked 81 | , HE.onClick (HE.input_ $ ToggleCompleted i) 82 | ] 83 | [ HH.text t.name 84 | ] 85 | where checked = if t.done then "checked" else "unchecked" 86 | 87 | eval :: Query ~> H.ComponentDSL State Query Message eff 88 | eval = case _ of 89 | UpdateNewTask newTaskName next -> do 90 | H.modify (_ { newTaskName = newTaskName }) 91 | pure next 92 | NewTask next -> do 93 | H.modify \st -> if st.newTaskName /= "" then st { tasks = st.tasks `snoc` { name: st.newTaskName, done: false }, newTaskName = "" } else st 94 | pure next 95 | ToggleCompleted i next -> do 96 | H.modify \st -> let newTasks = fromMaybe st.tasks $ modifyAt i (\t -> t { done = not t.done }) st.tasks in st { tasks = newTasks, numCompleted = length $ filter (\t -> t.done) newTasks } 97 | pure next 98 | RemoveCompleted next -> do 99 | H.modify \st -> st { tasks = filter (\t -> not t.done) st.tasks, numCompleted = 0 } 100 | pure next 101 | -------------------------------------------------------------------------------- /examples/todo-ps/src/Main.purs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Prelude 4 | import Control.Monad.Eff (Eff) 5 | import Halogen.Aff as HA 6 | import Halogen.VDom.Driver (runUI) 7 | import Component.Todo (component) 8 | 9 | main :: Eff (HA.HalogenEffects ()) Unit 10 | main = HA.runHalogenAff do 11 | body <- HA.awaitBody 12 | runUI component unit body 13 | -------------------------------------------------------------------------------- /examples/todo-ps/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | font-size: 28px; 6 | font-family: sans-serif; 7 | } 8 | 9 | html, body { 10 | height: 100%; 11 | overflow: none; 12 | } 13 | 14 | .ie-upgrade-container { 15 | width: 100%; 16 | height: 100%; 17 | font-family: Arial, sans-serif; 18 | font-size: 32px; 19 | color: #ffffff; 20 | background-color: #1ebbee; 21 | padding: 3em 1em 1em 1em; 22 | } 23 | .ie-upgrade-link { 24 | margin: 2em 0em; 25 | padding: 0 1em; 26 | color: #1ebbee; 27 | background-color: #ffffff; 28 | font-weight: bold; 29 | text-align: center; 30 | display: block; 31 | width: 100%; 32 | height: 2em; 33 | line-height: 2em; 34 | text-transform: uppercase; 35 | } 36 | 37 | .container { 38 | width: 100%; 39 | height: 100%; 40 | background-color: #9c27b0; 41 | } 42 | 43 | .text-input-wrapper { 44 | padding: 0.5em; 45 | position: fixed; 46 | top: 0; 47 | left: 0; 48 | righ: 0; 49 | } 50 | 51 | .text-input { 52 | width: 100%; 53 | line-height: 1.5em; 54 | padding: 0 0.2em; 55 | height: 1.5em; 56 | outline: none; 57 | border: none; 58 | color: #4a148c; 59 | background-color: rgba(255, 255, 255, 0.87); 60 | } 61 | .text-input:focus { 62 | background-color: #ffffff; 63 | } 64 | 65 | .task-list { 66 | overflow-y: auto; 67 | position: fixed; 68 | top: 2.5em; 69 | bottom: 1.2em; 70 | left: 0; 71 | right: 0; 72 | } 73 | 74 | .task-item { 75 | height: 1.5em; 76 | color: rgba(255, 255, 255, 0.87); 77 | padding: 0.5em; 78 | cursor: pointer; 79 | } 80 | .task-item.checked { 81 | text-decoration: line-through; 82 | color: rgba(255, 255, 255, 0.38); 83 | } 84 | .footer { 85 | position: fixed; 86 | left: 0; 87 | bottom: 0; 88 | right: 0; 89 | background-color: #ffffff; 90 | color: #9c27b0; 91 | } 92 | .btn-clear-tasks { 93 | width: 100%; 94 | text-align: center; 95 | font-size: 18px; 96 | height: 2.5em; 97 | line-height: 2.5em; 98 | text-transform: uppercase; 99 | cursor: pointer; 100 | } 101 | -------------------------------------------------------------------------------- /examples/todo-purescript.rs: -------------------------------------------------------------------------------- 1 | #![windows_subsystem = "windows"] 2 | 3 | extern crate web_view; 4 | 5 | use web_view::*; 6 | 7 | fn main() { 8 | let size = (320, 480); 9 | let resizable = false; 10 | let debug = true; 11 | let init_cb = |_webview| {}; 12 | let frontend_cb = |_webview: &mut _, _arg: &_, _userdata: &mut _| {}; 13 | let userdata = (); 14 | let html = include_str!("todo-ps/dist/bundle.html"); 15 | run("Rust / PureScript - Todo App", Content::Html(html), Some(size), resizable, debug, init_cb, frontend_cb, userdata); 16 | } 17 | -------------------------------------------------------------------------------- /examples/todo.rs: -------------------------------------------------------------------------------- 1 | // #![windows_subsystem = "windows"] 2 | 3 | #[macro_use] extern crate serde_derive; 4 | extern crate serde_json; 5 | extern crate web_view; 6 | 7 | use web_view::*; 8 | 9 | fn main() { 10 | let html = format!(r#" 11 | 12 | 13 | 14 | {styles} 15 | 16 | 17 | 23 | 24 | {scripts} 25 | 26 | 27 | 28 | "#, 29 | styles = inline_style(include_str!("todo/styles.css")), 30 | scripts = inline_script(include_str!("todo/picodom.js")) + &inline_script(include_str!("todo/app.js")), 31 | ); 32 | let size = (320, 480); 33 | let resizable = false; 34 | let debug = true; 35 | let init_cb = |_webview| {}; 36 | let userdata = vec![]; 37 | let (tasks, _) = run("Rust Todo App", Content::Html(html), Some(size), resizable, debug, init_cb, |webview, arg, tasks: &mut Vec| { 38 | use Cmd::*; 39 | match serde_json::from_str(arg).unwrap() { 40 | init => (), 41 | log { text } => println!("{}", text), 42 | addTask { name } => tasks.push(Task { name, done: false }), 43 | markTask { index, done } => tasks[index].done = done, 44 | clearDoneTasks => tasks.retain(|t| !t.done), 45 | } 46 | render(webview, tasks); 47 | }, userdata); 48 | println!("final state: {:?}", tasks); 49 | } 50 | 51 | fn render<'a, T>(webview: &mut WebView<'a, T>, tasks: &[Task]) { 52 | println!("{:#?}", tasks); 53 | webview.eval(&format!("rpc.render({})", serde_json::to_string(tasks).unwrap())); 54 | } 55 | 56 | #[derive(Debug, Serialize, Deserialize)] 57 | struct Task { 58 | name: String, 59 | done: bool, 60 | } 61 | 62 | #[allow(non_camel_case_types)] 63 | #[derive(Deserialize)] 64 | #[serde(tag = "cmd")] 65 | pub enum Cmd { 66 | init, 67 | log { text: String }, 68 | addTask { name: String }, 69 | markTask { index: usize, done: bool }, 70 | clearDoneTasks, 71 | } 72 | 73 | fn inline_style(s: &str) -> String { 74 | format!(r#""#, s) 75 | } 76 | 77 | fn inline_script(s: &str) -> String { 78 | format!(r#""#, s) 79 | } -------------------------------------------------------------------------------- /examples/todo/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function UI(items) { 4 | var h = picodom.h; 5 | function submit(e) { 6 | e.preventDefault(); 7 | e.stopImmediatePropagation(); 8 | var el = document.getElementById('task-name-input'); 9 | rpc.addTask(el.value); 10 | el.value = ''; 11 | } 12 | function clearTasks() { rpc.clearDoneTasks(); } 13 | function markTask(i){return function() { rpc.markTask(i, !items[i].done); }}; 14 | 15 | var taskItems = []; 16 | for (var i = 0; i < items.length; i++) { 17 | var checked = (items[i].done ? 'checked' : 'unchecked'); 18 | taskItems.push( 19 | h('div', {className : 'task-item ' + checked, onclick : markTask(i)}, 20 | items[i].name)); 21 | } 22 | 23 | return h('div', {className : 'container'}, 24 | h('form', {className : 'text-input-wrapper', onsubmit : submit}, 25 | h('input', { 26 | id : 'task-name-input', 27 | className : 'text-input', 28 | type : 'text', 29 | autofocus : true 30 | })), 31 | h('div', {className : 'task-list'}, taskItems), 32 | h('div', {className : 'footer'}, 33 | h('div', {className : 'btn-clear-tasks', onclick : clearTasks}, 34 | 'Delete completed'))); 35 | } 36 | 37 | var element; 38 | var oldNode; 39 | var rpc = { 40 | invoke : function(arg) { window.external.invoke(JSON.stringify(arg)); }, 41 | init : function() { rpc.invoke({cmd : 'init'}); }, 42 | log : function() { 43 | var s = ''; 44 | for (var i = 0; i < arguments.length; i++) { 45 | if (i != 0) { 46 | s = s + ' '; 47 | } 48 | s = s + JSON.stringify(arguments[i]); 49 | } 50 | rpc.invoke({cmd : 'log', text : s}); 51 | }, 52 | addTask : function(name) { rpc.invoke({cmd : 'addTask', name : name}); }, 53 | clearDoneTasks : function() { rpc.invoke({cmd : 'clearDoneTasks'}); }, 54 | markTask : function(index, done) { 55 | rpc.invoke({cmd : 'markTask', index : index, done : done}); 56 | }, 57 | render : function(items) { 58 | return element = picodom.patch(oldNode, (oldNode = UI(items)), element); 59 | }, 60 | }; 61 | 62 | window.onload = function() { rpc.init(); }; 63 | -------------------------------------------------------------------------------- /examples/todo/picodom.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t(e.picodom={})}(this,function(e){"use strict";function t(e,t){var n,r=[];for(u=arguments.length;u-- >2;)d.push(arguments[u]);for(;d.length;)if(Array.isArray(n=d.pop()))for(u=n.length;u--;)d.push(n[u]);else null!=n&&!0!==n&&!1!==n&&("number"==typeof n&&(n+=""),r.push(n));return"string"==typeof e?{tag:e,data:t||{},children:r}:e(t,r)}function n(e,t,a,l,u,d){if(null==a)t=e.insertBefore(o(l,u),t);else if(null!=l.tag&&l.tag===a.tag){i(t,a.data,l.data),u=u||"svg"===l.tag;for(var c=l.children.length,s=a.children.length,h={},v=[],p={},g=0;g (DialogType, DialogFlags) { 30 | match self { 31 | Dialog::SaveFile => (DialogType::Save, DialogFlags::FILE), 32 | Dialog::OpenFile => (DialogType::Open, DialogFlags::FILE), 33 | Dialog::ChooseDirectory => (DialogType::Open, DialogFlags::DIRECTORY), 34 | Dialog::Alert(alert) => { 35 | match alert { 36 | Alert::Info => (DialogType::Alert, DialogFlags::INFO), 37 | Alert::Warning => (DialogType::Alert, DialogFlags::WARNING), 38 | Alert::Error => (DialogType::Alert, DialogFlags::ERROR), 39 | } 40 | }, 41 | } 42 | } 43 | } 44 | 45 | /// Wrapper around content that can be displayed inside webview. 46 | /// Can be either Url or Html. 47 | /// Url fetches contents from address and displays it. 48 | /// Html displays strings contents. 49 | pub enum Content> { 50 | Url(T), 51 | Html(T), 52 | } 53 | 54 | pub fn run<'a, T: 'a, 55 | I: FnOnce(MyUnique>), 56 | F: FnMut(&mut WebView<'a, T>, &str, &mut T) + 'a, 57 | C: AsRef 58 | >( 59 | title: &str, content: Content, size: Option<(i32, i32)>, resizable: bool, debug: bool, titlebar_transparent: bool, init_cb: I, ext_cb: F, user_data: T 60 | ) -> (T, bool) { 61 | let (width, height) = size.unwrap_or((800, 600)); 62 | let fullscreen = size.is_none(); 63 | let title = CString::new(title).unwrap(); 64 | let url = match content { 65 | Content::Url(url) => CString::new(url.as_ref()).unwrap(), 66 | Content::Html(html) => CString::new(format!("data:text/html,{}", encode(html.as_ref()))).unwrap(), 67 | }; 68 | let mut handler_data = Box::new(HandlerData { 69 | ext_cb: Box::new(ext_cb), 70 | index: 0, 71 | dispatched_cbs: Default::default(), 72 | user_data 73 | }); 74 | let webview = unsafe { 75 | wrapper_webview_new( 76 | title.as_ptr(), url.as_ptr(), width, height, resizable as c_int, debug as c_int, titlebar_transparent as c_int, 77 | Some(transmute(handler_ext:: as ExternalInvokeFn)), 78 | &mut *handler_data as *mut _ as *mut c_void 79 | ) 80 | }; 81 | if webview.is_null() { 82 | (handler_data.user_data, false) 83 | } else { 84 | unsafe { webview_set_fullscreen(webview, fullscreen as _); } 85 | init_cb(MyUnique(webview as _)); 86 | unsafe { 87 | while webview_loop(webview, 1) == 0 {} 88 | webview_exit(webview); 89 | wrapper_webview_free(webview); 90 | } 91 | (handler_data.user_data, true) 92 | } 93 | } 94 | 95 | struct HandlerData<'a, T: 'a> { 96 | ext_cb: Box, &str, &mut T) + 'a>, 97 | index: usize, 98 | dispatched_cbs: HashMap, &mut T) + Send + 'a>>, 99 | user_data: T 100 | } 101 | 102 | pub struct WebView<'a, T: 'a>(PhantomData<&'a mut T>); 103 | 104 | pub struct MyUnique(*mut T); 105 | unsafe impl Send for MyUnique {} 106 | unsafe impl Sync for MyUnique {} 107 | 108 | impl<'a, T> MyUnique> { 109 | #[inline(always)] 110 | pub fn dispatch FnMut(&mut WebView<'b, T>, &mut T) + Send /*+ 'a*/>(&self, f: F) { 111 | unsafe { &mut *self.0 }.dispatch(f); 112 | } 113 | 114 | pub fn set_background_color(&mut self, r: f32, g: f32, b: f32, a: f32) { 115 | unsafe { &mut *self.0 }.set_background_color(r, g, b, a); 116 | } 117 | } 118 | 119 | impl<'a, T> WebView<'a, T> { 120 | #[inline(always)] 121 | fn erase(&mut self) -> *mut CWebView { self as *mut _ as *mut _ } 122 | 123 | #[inline(always)] 124 | fn get_userdata(&mut self) -> &mut HandlerData { 125 | let user_data = unsafe { wrapper_webview_get_userdata(self.erase()) }; 126 | let data: &mut HandlerData = unsafe { &mut *(user_data as *mut HandlerData) }; 127 | data 128 | } 129 | 130 | pub fn set_background_color(&mut self, r: f32, g: f32, b: f32, a: f32) { 131 | unsafe { webview_set_background_color(self.erase(), r as c_float, g as c_float, b as c_float, a as c_float) } 132 | } 133 | 134 | pub fn terminate(&mut self) { 135 | unsafe { webview_terminate(self.erase()) } 136 | } 137 | 138 | pub fn dispatch FnMut(&mut WebView<'b, T>, &mut T) + Send /*+ 'a*/>(&'a mut self, f: F) { 139 | let erased = self.erase(); 140 | let index = { 141 | let data = self.get_userdata(); 142 | let index = data.index; 143 | data.index += 1; 144 | data.dispatched_cbs.insert(index, Box::new(f)); 145 | index 146 | }; 147 | unsafe { 148 | webview_dispatch(erased, Some(transmute(handler_dispatch as DispatchFn)), index as _) 149 | } 150 | } 151 | 152 | pub fn eval(&mut self, js: &str) -> i32 { 153 | let js = CString::new(js).unwrap(); 154 | unsafe { webview_eval(self.erase(), js.as_ptr()) } 155 | } 156 | 157 | pub fn inject_css(&mut self, css: &str) -> i32 { 158 | let css = CString::new(css).unwrap(); 159 | unsafe { webview_inject_css(self.erase(), css.as_ptr()) } 160 | } 161 | 162 | pub fn dialog(&mut self, dialog: Dialog, title: &str, arg: Option<&str>) -> String { 163 | let (dtype, dflags) = dialog.parameters(); 164 | let title = CString::new(title).unwrap(); 165 | let arg = CString::new(arg.unwrap_or("")).unwrap(); 166 | let buffer_size = 4096; 167 | let mut buffer = Vec::with_capacity(buffer_size); 168 | buffer.push(0); // If cancel is pressed nothing is written to the buffer. 169 | let result = buffer.as_mut_ptr(); 170 | forget(buffer); 171 | 172 | unsafe { 173 | webview_dialog( 174 | self.erase(), 175 | dtype, 176 | dflags, 177 | title.as_ptr(), 178 | arg.as_ptr(), 179 | result, 180 | buffer_size 181 | ); 182 | } 183 | 184 | let mut result = unsafe { Vec::from_raw_parts(result, buffer_size, buffer_size) }; 185 | let len = result.iter().position(|&c| c == 0).unwrap(); 186 | result.truncate(len); 187 | result.shrink_to_fit(); // the space allocated is probably an order of a magnitude larger than the path 188 | 189 | unsafe { String::from_utf8_unchecked(transmute(result)) } // invalid UTF-8 is an OS bug 190 | } 191 | } 192 | 193 | type ExternalInvokeFn<'a, T> = extern "system" fn(webview: *mut WebView<'a, T>, arg: *const c_char); 194 | type DispatchFn<'a, T> = extern "system" fn(webview: *mut WebView<'a, T>, arg: *mut c_void); 195 | 196 | 197 | extern "system" fn handler_dispatch<'a, T>(webview: *mut WebView<'a, T>, arg: *mut c_void) { 198 | let data = unsafe { (*webview).get_userdata() }; 199 | let i = arg as _; 200 | use std::collections::hash_map::Entry; 201 | if let Entry::Occupied(mut e) = data.dispatched_cbs.entry(i) { 202 | e.get_mut()(unsafe { &mut *webview }, &mut data.user_data); 203 | e.remove_entry(); 204 | } else { 205 | unreachable!(); 206 | } 207 | } 208 | 209 | extern "system" fn handler_ext<'a, T>(webview: *mut WebView<'a, T>, arg: *const c_char) { 210 | let data = unsafe { (*webview).get_userdata() }; 211 | let arg = unsafe { CStr::from_ptr(arg) }.to_string_lossy().to_string(); 212 | (data.ext_cb)(unsafe { &mut *webview }, &arg, &mut data.user_data); 213 | } 214 | -------------------------------------------------------------------------------- /webview-sys/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "webview-sys" 3 | version = "0.1.0" 4 | authors = ["Boscop"] 5 | license = "MIT" 6 | repository = "https://github.com/Boscop/web-view" 7 | description = "Rust native ffi bindings for webview" 8 | keywords = ["web", "gui", "desktop", "electron", "webkit"] 9 | categories = ["gui", "web-programming", "api-bindings", "rendering", "visualization"] 10 | build = "build.rs" 11 | links = "webview" 12 | 13 | [lib] 14 | name = "webview_sys" 15 | path = "lib.rs" 16 | 17 | [dependencies] 18 | bitflags = "1.0" 19 | 20 | [build-dependencies] 21 | cc = "1" 22 | pkg-config = "0.3" -------------------------------------------------------------------------------- /webview-sys/build.rs: -------------------------------------------------------------------------------- 1 | extern crate cc; 2 | extern crate pkg_config; 3 | 4 | use std::env; 5 | use std::path::Path; 6 | use std::process::Command; 7 | 8 | fn main() { 9 | // Initialize webview submodule if user forgot to clone parent repository with --recursive. 10 | if !Path::new("webview/.git").exists() { 11 | let _ = Command::new("git").args(&["submodule", "update", "--init"]).status(); 12 | } 13 | 14 | let mut build = cc::Build::new(); 15 | build.flag_if_supported("-std=c11"); 16 | build.include("webview"); 17 | build.file("webview.c"); 18 | if env::var("DEBUG").is_err() { 19 | build.define("NDEBUG", None); 20 | } else { 21 | build.define("DEBUG", None); 22 | } 23 | let target = env::var("TARGET").unwrap(); 24 | if target.contains("windows") { 25 | build.define("WEBVIEW_WINAPI", None); 26 | for &lib in &["ole32", "comctl32", "oleaut32", "uuid", "gdi32"] { 27 | println!("cargo:rustc-link-lib={}", lib); 28 | } 29 | } else if target.contains("linux") || target.contains("bsd") { 30 | let webkit = pkg_config::Config::new().atleast_version("2.8").probe("webkit2gtk-4.0").unwrap(); 31 | 32 | for path in webkit.include_paths { 33 | build.include(path); 34 | } 35 | build.define("WEBVIEW_GTK", None); 36 | } else if target.contains("apple") { 37 | build.define("WEBVIEW_COCOA", None); 38 | build.flag("-x"); 39 | build.flag("objective-c"); 40 | println!("cargo:rustc-link-lib=framework=Cocoa"); 41 | println!("cargo:rustc-link-lib=framework=WebKit"); 42 | } else { 43 | panic!("unsupported target"); 44 | } 45 | build.compile("webview"); 46 | } 47 | -------------------------------------------------------------------------------- /webview-sys/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate bitflags; 3 | 4 | use std::os::raw::*; 5 | 6 | pub enum CWebView {} // opaque type, only used in ffi pointers 7 | 8 | type ErasedExternalInvokeFn = extern "system" fn(webview: *mut CWebView, arg: *const c_char); 9 | type ErasedDispatchFn = extern "system" fn(webview: *mut CWebView, arg: *mut c_void); 10 | 11 | #[repr(C)] 12 | pub enum DialogType { 13 | Open = 0, 14 | Save = 1, 15 | Alert = 2, 16 | } 17 | 18 | bitflags! { 19 | #[repr(C)] 20 | pub struct DialogFlags: u32 { 21 | const FILE = 0b0000; 22 | const DIRECTORY = 0b0001; 23 | const INFO = 0b0010; 24 | const WARNING = 0b0100; 25 | const ERROR = 0b0110; 26 | } 27 | } 28 | 29 | extern { 30 | pub fn wrapper_webview_free(this: *mut CWebView); 31 | pub fn wrapper_webview_new(title: *const c_char, url: *const c_char, width: c_int, height: c_int, resizable: c_int, debug: c_int, titlebar_transparent: c_int, external_invoke_cb: Option, userdata: *mut c_void) -> *mut CWebView; 32 | pub fn webview_loop(this: *mut CWebView, blocking: c_int) -> c_int; 33 | pub fn webview_terminate(this: *mut CWebView); 34 | pub fn webview_exit(this: *mut CWebView); 35 | pub fn wrapper_webview_get_userdata(this: *mut CWebView) -> *mut c_void; 36 | pub fn webview_dispatch(this: *mut CWebView, f: Option, arg: *mut c_void); 37 | pub fn webview_eval(this: *mut CWebView, js: *const c_char) -> c_int; 38 | pub fn webview_inject_css(this: *mut CWebView, css: *const c_char) -> c_int; 39 | pub fn webview_set_fullscreen(this: *mut CWebView, fullscreen: c_int); 40 | pub fn webview_dialog(this: *mut CWebView, dialog_type: DialogType, flags: DialogFlags, title: *const c_char, arg: *const c_char, result: *mut c_char, result_size: usize); 41 | pub fn webview_set_background_color(this: *mut CWebView, r: c_float, g: c_float, b: c_float, a: c_float); 42 | } 43 | -------------------------------------------------------------------------------- /webview-sys/webview.c: -------------------------------------------------------------------------------- 1 | #define WEBVIEW_IMPLEMENTATION 2 | #include "webview.h" 3 | 4 | void wrapper_webview_free(struct webview* w) { 5 | free(w); 6 | } 7 | 8 | struct webview* wrapper_webview_new(const char* title, const char* url, int width, int height, int resizable, int debug, int titlebar_transparent, webview_external_invoke_cb_t external_invoke_cb, void* userdata) { 9 | struct webview* w = (struct webview*)calloc(1, sizeof(*w)); 10 | w->width = width; 11 | w->height = height; 12 | w->title = title; 13 | w->url = url; 14 | w->resizable = resizable; 15 | w->debug = debug; 16 | w->titlebar_transparent = titlebar_transparent; 17 | w->external_invoke_cb = external_invoke_cb; 18 | w->userdata = userdata; 19 | if (webview_init(w) != 0) { 20 | wrapper_webview_free(w); 21 | return NULL; 22 | } 23 | return w; 24 | } 25 | 26 | void* wrapper_webview_get_userdata(struct webview* w) { 27 | return w->userdata; 28 | } 29 | 30 | --------------------------------------------------------------------------------