Skip to content

Commit

Permalink
Wasm support (#759)
Browse files Browse the repository at this point in the history
* Add wasm backend + fix std::time dep

A basic wasm backend is added as a continuation of #30.
Since std::time isn't available on wasm, an additional dependency is
added to abstract std::time::Instant to work with wasm.

* Disable simple logger on wasm

This commit ensures that all examples run with minimal changes.
More specifically we don't need to remove the .use_simple_logger() call
on all Apps.

* Fix resizing in wasm backends

The resize callback should be registered to the window, since resizing
the window doesn't call the resize callback for canvases even if those
do get resized.

* Added scrolling support + fixed hidpi handling

The scroll example now works as expected on hi and low dpi screens.

* Use explicit lifetime in StrOrChar

This makes passing it easier to pass owned strings when creating a new
KeyEvent, which is necessary when interfacing with WASM.

* Prevent the browser from going back on backspace

This should be the default behavior to make text widgets usable.
At a later iteration it may be better to only disable the browser from
going back when a widget is selected that is expecting keyboard input
where backspace is intended for something else.

I expect this functionality would require some way of accessing the
web_sys event from within a druid callback.

* Fix key text + require console_log + adjust style

This commit addresses comments in the code review:

Key text:
  - web_sys returns the name of each pressed key from which we can
    generate a viable printable string. This mechanism is revised to
    include all non-printable keys as listed in MDN.
  - notably, the tab character and numpad characters have gained
    printable text in this commit.

console_log:
  - console_log is not required in wasm builds.
  - integrated console_log with the use_simple_logger api. `console_log`
    is initialized with log level `trace` as is done in simple logger by
    default.

Style:
  - non std use statements are now before third party use statements.
  - key_to_text function signature refactor
  - specify the log module when calling log::{warn, error} explicitly.

* Fix up console_log dependency

Moved console_log dependency from druid-shell to druid, since this is
where it is initialized. This fixes the wasm build.

* Fix the build script for wasm examples

This addresses the CI failures caused by the weird build of the wasm
examples.

To elaborate:
In order to include each of the examples in the wasm example, a
symlink to the examples directory is created in build.rs along with the
corresponding examples.rs module which declares examples known to work
with wasm. To satisfy CI, the examples.rs committed to the repo must be
empty since rustfmt does not call "build.rs".

* Add .gitignore to wasm examples + fix clippy bugs

The included .gitignore ignores the generated examples module. The
examples.rs should remain committed, but it need not be ever changed.

* Fix x11 keycodes StrOrChar conversion

The newly introduced lifetime parameter must be specified exmplicitly.


* Add --no-run to `cargo test` for wasm targets

wasm targets cannot be run in the normal way.

* Do not build the unit test module for wasm32

The tests cannot be run in the normal way anyways. Leaving this for a
future PR to flush out.

* Install necessary deps in wasm CI for macos/ubuntu

* Add warnings for unimplemented file ops in web backend

This commit exposes the error of the missing implementation for file dialogs in the web backend using log::warn. This is a temporary solution until the `open_file_sync` and `save_as_sync` functions propagate the error downstream using Result instead of Option.

* Rework the generated examples in the wasm example

This commit attempts a different approach at including the automatically
generated examples from the parent directory.
This method satisfies both rustfmt and cargo test as before, but it also
doesn't modify any files in the source tree, keeping the diff clean
after a build.

* Rename switch example js entry point for wasm build

This commit fixes the generated html file for the switch example to load
"switch_demo" instead of "switch" for the switch example. The word
switch conflicts with JavaScript's switch statement, which is the reason
for this awkwardness.

Co-authored-by: Leopold Luley <[email protected]>
  • Loading branch information
elrnv and luleyleo authored Apr 15, 2020
1 parent ee55405 commit 37b9646
Show file tree
Hide file tree
Showing 55 changed files with 1,922 additions and 103 deletions.
59 changes: 59 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -239,3 +239,62 @@ jobs:
with:
command: rustc
args: --manifest-path=druid-shell/Cargo.toml --features=x11 -- -D warnings

test-stable-wasm:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macOS-latest, windows-2019, ubuntu-latest]

name: cargo test stable (wasm32)
steps:
- uses: actions/checkout@v2

- name: install cairo
run: brew install cairo
if: contains(matrix.os, 'mac')

- name: install libgtk-dev
run: |
sudo apt update
sudo apt install libgtk-3-dev
if: contains(matrix.os, 'ubuntu')

- name: install deps
run: cargo install wasm-pack

- name: install stable toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: wasm32-unknown-unknown
components: clippy
profile: minimal
override: true

- name: cargo clippy (wasm32)
uses: actions-rs/cargo@v1
with:
command: clippy
args: --all --target wasm32-unknown-unknown -- -D warnings

- name: cargo test compile druid-shell (wasm32)
uses: actions-rs/cargo@v1
with:
command: test
args: --manifest-path=druid-shell/Cargo.toml --no-run --target wasm32-unknown-unknown

- name: cargo test compile druid (wasm32)
uses: actions-rs/cargo@v1
with:
command: test
args: --manifest-path=druid/Cargo.toml --no-run --target wasm32-unknown-unknown

- name: wasm-pack build examples
run: wasm-pack build --target web druid/examples/wasm

- name: Run rustc -D warnings in druid/examples/wasm
uses: actions-rs/cargo@v1
with:
command: rustc
args: --manifest-path=druid/examples/wasm/Cargo.toml -- -D warnings
101 changes: 74 additions & 27 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ members = [
"druid-shell",
"druid-derive",
"docs/book_examples",
"druid/examples/wasm",
]
7 changes: 7 additions & 0 deletions druid-shell/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ time = "0.2.7"
cfg-if = "0.1.10"
# NOTE: if changing, ensure version is compatible with the version in piet
kurbo = "0.5.11"
# NOTE: This defaults to mimicking std::time on non-wasm targets
instant = { version = "0.1", features = [ "wasm-bindgen" ] }

cairo-rs = { version = "0.8.1", default_features = false, optional = true }
cairo-sys-rs = { version = "0.9.2", default_features = false, optional = true }
Expand Down Expand Up @@ -60,3 +62,8 @@ gtk-sys = "0.9.0"
[target.'cfg(target_os="linux")'.dependencies.gtk]
version = "0.8.1"
features = ["v3_20"]

[target.'cfg(target_arch="wasm32")'.dependencies]
wasm-bindgen = "0.2.59"
js-sys = "0.3.36"
web-sys = { version = "0.3.36", features = ["Window", "MouseEvent", "CssStyleDeclaration", "WheelEvent", "KeyEvent", "KeyboardEvent"] }
2 changes: 1 addition & 1 deletion druid-shell/examples/shello.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ impl WinHandler for HelloState {
}

fn key_down(&mut self, event: KeyEvent) -> bool {
let deadline = std::time::Instant::now() + std::time::Duration::from_millis(500);
let deadline = instant::Instant::now() + std::time::Duration::from_millis(500);
let id = self.handle.request_timer(deadline);
println!("keydown: {:?}, timer id = {:?}", event, id);

Expand Down
22 changes: 11 additions & 11 deletions druid-shell/src/keyboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,12 @@ pub struct KeyEvent {
impl KeyEvent {
/// Create a new `KeyEvent` struct. This accepts either &str or char for the last
/// two arguments.
pub(crate) fn new(
pub(crate) fn new<'a>(
key_code: impl Into<KeyCode>,
is_repeat: bool,
mods: KeyModifiers,
text: impl Into<StrOrChar>,
unmodified_text: impl Into<StrOrChar>,
text: impl Into<StrOrChar<'a>>,
unmodified_text: impl Into<StrOrChar<'a>>,
) -> Self {
let text = match text.into() {
StrOrChar::Char(c) => c.into(),
Expand Down Expand Up @@ -176,25 +176,25 @@ impl From<char> for TinyStr {

/// A type we use in the constructor of `KeyEvent`, specifically to avoid exposing
/// internals.
pub enum StrOrChar {
pub enum StrOrChar<'a> {
Char(char),
Str(&'static str),
Str(&'a str),
}

impl From<&'static str> for StrOrChar {
fn from(src: &'static str) -> Self {
impl<'a> From<&'a str> for StrOrChar<'a> {
fn from(src: &'a str) -> Self {
StrOrChar::Str(src)
}
}

impl From<char> for StrOrChar {
fn from(src: char) -> StrOrChar {
impl From<char> for StrOrChar<'static> {
fn from(src: char) -> StrOrChar<'static> {
StrOrChar::Char(src)
}
}

impl From<Option<char>> for StrOrChar {
fn from(src: Option<char>) -> StrOrChar {
impl From<Option<char>> for StrOrChar<'static> {
fn from(src: Option<char>) -> StrOrChar<'static> {
match src {
Some(c) => StrOrChar::Char(c),
None => StrOrChar::Str(""),
Expand Down
3 changes: 3 additions & 0 deletions druid-shell/src/platform/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,8 @@ cfg_if::cfg_if! {
} else if #[cfg(target_os = "linux")] {
mod gtk;
pub use self::gtk::*;
} else if #[cfg(target_arch = "wasm32")] {
mod web;
pub use web::*;
}
}
39 changes: 39 additions & 0 deletions druid-shell/src/platform/web/application.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright 2020 The xi-editor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Web implementation of features at the application scope.
use super::clipboard::Clipboard;
use crate::application::AppHandler;

pub struct Application;

impl Application {
pub fn new(_handler: Option<Box<dyn AppHandler>>) -> Application {
Application
}

pub fn run(&mut self) {}

pub fn quit() {}

pub fn clipboard() -> Clipboard {
Clipboard
}

pub fn get_locale() -> String {
//TODO ahem
"en-US".into()
}
}
60 changes: 60 additions & 0 deletions druid-shell/src/platform/web/clipboard.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright 2020 The xi-editor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Interactions with the browser pasteboard.
use crate::clipboard::{ClipboardFormat, FormatId};

/// The browser clipboard.
#[derive(Debug, Clone, Default)]
pub struct Clipboard;

impl Clipboard {
/// Put a string onto the system clipboard.
pub fn put_string(&mut self, _s: impl AsRef<str>) {
log::warn!("unimplemented");
}

/// Put multi-format data on the system clipboard.
pub fn put_formats(&mut self, _formats: &[ClipboardFormat]) {
log::warn!("unimplemented");
}

/// Get a string from the system clipboard, if one is available.
pub fn get_string(&self) -> Option<String> {
log::warn!("unimplemented");
None
}

/// Given a list of supported clipboard types, returns the supported type which has
/// highest priority on the system clipboard, or `None` if no types are supported.
pub fn preferred_format(&self, _formats: &[FormatId]) -> Option<FormatId> {
log::warn!("unimplemented");
None
}

/// Return data in a given format, if available.
///
/// It is recommended that the `fmt` argument be a format returned by
/// [`Clipboard::preferred_format`]
pub fn get_format(&self, _format: FormatId) -> Option<Vec<u8>> {
log::warn!("unimplemented");
None
}

pub fn available_type_names(&self) -> Vec<String> {
log::warn!("unimplemented");
Vec::new()
}
}
50 changes: 50 additions & 0 deletions druid-shell/src/platform/web/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright 2020 The xi-editor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Web platform errors.
use wasm_bindgen::JsValue;

#[derive(Debug, Clone)]
pub enum Error {
NoWindow,
NoDocument,
Js(JsValue),
JsCast,
NoElementById(String),
NoContext,
Unimplemented,
}

impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Error::NoWindow => write!(f, "No global window found"),
Error::NoDocument => write!(f, "No global document found"),
Error::Js(err) => write!(f, "JavaScript error: {:?}", err.as_string()),
Error::JsCast => write!(f, "JavaScript cast error"),
Error::NoElementById(err) => write!(f, "get_element_by_id error: {}", err),
Error::NoContext => write!(f, "Failed to get a draw context"),
Error::Unimplemented => write!(f, "Requested an unimplemented feature"),
}
}
}

impl From<JsValue> for Error {
fn from(js: JsValue) -> Error {
Error::Js(js)
}
}

impl std::error::Error for Error {}
Loading

0 comments on commit 37b9646

Please sign in to comment.