diff --git a/Cargo.toml b/Cargo.toml index 9c679c853..c2d8c34a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,11 +34,13 @@ members = [ # Full project examples "examples/tailwind", "examples/PWA-example", + # "examples/ios_demo", # Playwrite tests "playwrite-tests/liveview", "playwrite-tests/web", "playwrite-tests/fullstack", ] +exclude = ["examples/ios_demo"] # dependencies that are shared across packages [workspace.dependencies] diff --git a/examples/ios_demo/.gitignore b/examples/ios_demo/.gitignore new file mode 100644 index 000000000..419513ee1 --- /dev/null +++ b/examples/ios_demo/.gitignore @@ -0,0 +1,10 @@ +# Rust +target/ +**/*.rs.bk + +# tauri-mobile +.cargo/ +/gen + +# macOS +.DS_Store diff --git a/examples/ios_demo/Cargo.toml b/examples/ios_demo/Cargo.toml new file mode 100644 index 000000000..8034ec8d8 --- /dev/null +++ b/examples/ios_demo/Cargo.toml @@ -0,0 +1,51 @@ +[package] +name = "rustnl-ios" +version = "0.1.0" +authors = ["Jonathan Kelley "] +edition = "2018" + +[lib] +crate-type = ["staticlib", "cdylib", "rlib"] + +[[bin]] +name = "rustnl-ios-desktop" +path = "gen/bin/desktop.rs" + +[package.metadata.cargo-android] +app-activity-name = "com.example.rustnl_ios.MainActivity" +app-dependencies = [ + "androidx.webkit:webkit:1.6.1", + "androidx.appcompat:appcompat:1.6.1", + "com.google.android.material:material:1.8.0", +] +project-dependencies = ["org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.21"] +app-plugins = ["org.jetbrains.kotlin.android"] +app-permissions = ["android.permission.INTERNET"] +app-theme-parent = "Theme.MaterialComponents.DayNight.DarkActionBar" +vulkan-validation = false + +[package.metadata.cargo-android.env-vars] +WRY_ANDROID_PACKAGE = "com.example.rustnl_ios" +WRY_ANDROID_LIBRARY = "rustnl_ios" +WRY_ANDROID_KOTLIN_FILES_OUT_DIR = "/app/src/main/kotlin/com/example/rustnl_ios" + +[package.metadata.cargo-apple.ios] +frameworks = ["WebKit"] + +[dependencies] +anyhow = "1.0.56" +log = "0.4.11" +im-rc = "15.1.0" +dioxus = { path = "../../packages/dioxus" } +dioxus-desktop = { path = "../../packages/desktop" } + +[target.'cfg(target_os = "android")'.dependencies] +android_logger = "0.9.0" +jni = "0.19.0" +paste = "1.0" + +[target.'cfg(not(target_os = "android"))'.dependencies] +env_logger = "0.9.0" + +[target.'cfg(target_os = "ios")'.dependencies] +core-foundation = "0.9.3" diff --git a/examples/ios_demo/README.md b/examples/ios_demo/README.md new file mode 100644 index 000000000..ebe26fa16 --- /dev/null +++ b/examples/ios_demo/README.md @@ -0,0 +1,9 @@ +# wry + +## iOS + +Must run Xcode on rosetta. Goto Application > Right Click Xcode > Get Info > Open in Rosetta. + +If you are using M1, you will have to run `cargo build --target x86_64-apple-ios` instead of `cargo apple build` if you want to run in simulator. + +Otherwise, it's all `cargo apple run` when running in actual device. diff --git a/examples/ios_demo/mobile.toml b/examples/ios_demo/mobile.toml new file mode 100644 index 000000000..062cf8a35 --- /dev/null +++ b/examples/ios_demo/mobile.toml @@ -0,0 +1,8 @@ +[app] +name = "rustnl-ios" +stylized-name = "Rustnl Ios" +domain = "example.com" +template-pack = "wry" + +[apple] +development-team = "34U4FG9TJ8" diff --git a/examples/ios_demo/src/lib.rs b/examples/ios_demo/src/lib.rs new file mode 100644 index 000000000..a45de6c2b --- /dev/null +++ b/examples/ios_demo/src/lib.rs @@ -0,0 +1,261 @@ +use anyhow::Result; +use dioxus::prelude::*; +use dioxus_elements::input_data::keyboard_types::Key; +#[cfg(target_os = "android")] +use wry::android_binding; + +pub fn main() -> Result<()> { + init_logging(); + + dioxus_desktop::launch(app); + Ok(()) +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { + div { "Hello World!" } + }) +} + +// #[derive(PartialEq, Eq, Clone, Copy)] +// pub enum FilterState { +// All, +// Active, +// Completed, +// } + +// #[derive(Debug, PartialEq, Eq, Clone)] +// pub struct TodoItem { +// pub id: u32, +// pub checked: bool, +// pub contents: String, +// } + +// pub fn app(cx: Scope<()>) -> Element { +// let todos = use_state(cx, im_rc::HashMap::::default); +// let filter = use_state(cx, || FilterState::All); +// let draft = use_state(cx, || "".to_string()); +// let todo_id = use_state(cx, || 0); + +// // Filter the todos based on the filter state +// let mut filtered_todos = todos +// .iter() +// .filter(|(_, item)| match **filter { +// FilterState::All => true, +// FilterState::Active => !item.checked, +// FilterState::Completed => item.checked, +// }) +// .map(|f| *f.0) +// .collect::>(); +// filtered_todos.sort_unstable(); + +// let active_todo_count = todos.values().filter(|item| !item.checked).count(); +// let active_todo_text = match active_todo_count { +// 1 => "item", +// _ => "items", +// }; + +// let show_clear_completed = todos.values().any(|todo| todo.checked); + +// let selected = |state| { +// if *filter == state { +// "selected" +// } else { +// "false" +// } +// }; + +// cx.render(rsx! { +// section { class: "todoapp", +// style { include_str!("./style.css") } +// header { class: "header", +// h1 {"todos"} +// input { +// class: "new-todo", +// placeholder: "What needs to be done?", +// value: "{draft}", +// autofocus: "true", +// oninput: move |evt| { +// draft.set(evt.value.clone()); +// }, +// onkeydown: move |evt| { +// if evt.key() == Key::Enter && !draft.is_empty() { +// todos.make_mut().insert( +// **todo_id, +// TodoItem { +// id: **todo_id, +// checked: false, +// contents: draft.to_string(), +// }, +// ); +// *todo_id.make_mut() += 1; +// draft.set("".to_string()); +// } +// } +// } +// } +// section { +// class: "main", +// if !todos.is_empty() { +// rsx! { +// input { +// id: "toggle-all", +// class: "toggle-all", +// r#type: "checkbox", +// onchange: move |_| { +// let check = active_todo_count != 0; +// for (_, item) in todos.make_mut().iter_mut() { +// item.checked = check; +// } +// }, +// checked: if active_todo_count == 0 { "true" } else { "false" }, +// } +// label { r#for: "toggle-all" } +// } +// } +// ul { class: "todo-list", +// filtered_todos.iter().map(|id| rsx!(TodoEntry { +// key: "{id}", +// id: *id, +// todos: todos, +// })) +// } +// (!todos.is_empty()).then(|| rsx!( +// footer { class: "footer", +// span { class: "todo-count", +// strong {"{active_todo_count} "} +// span {"{active_todo_text} left"} +// } +// ul { class: "filters", +// for (state, state_text, url) in [ +// (FilterState::All, "All", "#/"), +// (FilterState::Active, "Active", "#/active"), +// (FilterState::Completed, "Completed", "#/completed"), +// ] { +// li { +// a { +// href: url, +// class: selected(state), +// onclick: move |_| filter.set(state), +// prevent_default: "onclick", +// state_text +// } +// } +// } +// } +// show_clear_completed.then(|| rsx!( +// button { +// class: "clear-completed", +// onclick: move |_| todos.make_mut().retain(|_, todo| !todo.checked), +// "Clear completed" +// } +// )) +// } +// )) +// } +// } +// footer { class: "info", +// p { "Double-click to edit a todo" } +// p { "Created by ", a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" }} +// p { "Part of ", a { href: "http://todomvc.com", "TodoMVC" }} +// } +// }) +// } + +// #[derive(Props)] +// pub struct TodoEntryProps<'a> { +// todos: &'a UseState>, +// id: u32, +// } + +// pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element { +// let is_editing = use_state(cx, || false); + +// let todos = cx.props.todos.get(); +// let todo = &todos[&cx.props.id]; +// let completed = if todo.checked { "completed" } else { "" }; +// let editing = if **is_editing { "editing" } else { "" }; + +// cx.render(rsx!{ +// li { +// class: "{completed} {editing}", +// div { class: "view", +// input { +// class: "toggle", +// r#type: "checkbox", +// id: "cbg-{todo.id}", +// checked: "{todo.checked}", +// oninput: move |evt| { +// cx.props.todos.make_mut()[&cx.props.id].checked = evt.value.parse().unwrap(); +// } +// } +// label { +// r#for: "cbg-{todo.id}", +// ondblclick: move |_| is_editing.set(true), +// prevent_default: "onclick", +// "{todo.contents}" +// } +// button { +// class: "destroy", +// onclick: move |_| { cx.props.todos.make_mut().remove(&todo.id); }, +// prevent_default: "onclick", +// } +// } +// is_editing.then(|| rsx!{ +// input { +// class: "edit", +// value: "{todo.contents}", +// oninput: move |evt| cx.props.todos.make_mut()[&cx.props.id].contents = evt.value.clone(), +// autofocus: "true", +// onfocusout: move |_| is_editing.set(false), +// onkeydown: move |evt| { +// match evt.key() { +// Key::Enter | Key::Escape | Key::Tab => is_editing.set(false), +// _ => {} +// } +// }, +// } +// }) +// } +// }) +// } + +#[cfg(target_os = "android")] +fn init_logging() { + android_logger::init_once( + android_logger::Config::default() + .with_min_level(log::Level::Trace) + .with_tag("rustnl-ios"), + ); +} + +#[cfg(not(target_os = "android"))] +fn init_logging() { + env_logger::init(); +} + +#[cfg(any(target_os = "android", target_os = "ios"))] +fn stop_unwind T, T>(f: F) -> T { + match std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) { + Ok(t) => t, + Err(err) => { + eprintln!("attempt to unwind out of `rust` with err: {:?}", err); + std::process::abort() + } + } +} + +#[cfg(any(target_os = "android", target_os = "ios"))] +fn _start_app() { + main().unwrap(); +} + +#[no_mangle] +#[inline(never)] +#[cfg(any(target_os = "android", target_os = "ios"))] +pub extern "C" fn start_app() { + #[cfg(target_os = "android")] + android_binding!(com_example, rustnl_ios, _start_app); + #[cfg(target_os = "ios")] + _start_app() +} diff --git a/examples/ios_demo/src/style.css b/examples/ios_demo/src/style.css new file mode 100644 index 000000000..ab9dbd7a9 --- /dev/null +++ b/examples/ios_demo/src/style.css @@ -0,0 +1,379 @@ +html, +body { + margin: 0; + padding: 0; +} + +button { + margin: 0; + padding: 0; + border: 0; + background: none; + font-size: 100%; + vertical-align: baseline; + font-family: inherit; + font-weight: inherit; + color: inherit; + -webkit-appearance: none; + appearance: none; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; + line-height: 1.4em; + background: #f5f5f5; + color: #4d4d4d; + min-width: 230px; + max-width: 550px; + margin: 0 auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-weight: 300; +} + +:focus { + outline: 0; +} + +.hidden { + display: none; +} + +.todoapp { + background: #fff; + margin: 130px 0 40px 0; + position: relative; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), + 0 25px 50px 0 rgba(0, 0, 0, 0.1); +} + +.todoapp input::-webkit-input-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp input::-moz-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp input::input-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp h1 { + position: absolute; + top: -155px; + width: 100%; + font-size: 100px; + font-weight: 100; + text-align: center; + color: rgba(175, 47, 47, 0.15); + -webkit-text-rendering: optimizeLegibility; + -moz-text-rendering: optimizeLegibility; + text-rendering: optimizeLegibility; +} + +.new-todo, +.edit { + position: relative; + margin: 0; + width: 100%; + font-size: 24px; + font-family: inherit; + font-weight: inherit; + line-height: 1.4em; + border: 0; + color: inherit; + padding: 6px; + border: 1px solid #999; + box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); + box-sizing: border-box; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.new-todo { + padding: 16px 16px 16px 60px; + border: none; + background: rgba(0, 0, 0, 0.003); + box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03); +} + +.main { + position: relative; + z-index: 2; + border-top: 1px solid #e6e6e6; +} + +.toggle-all { + text-align: center; + border: none; + /* Mobile Safari */ + opacity: 0; + position: absolute; +} + +.toggle-all+label { + width: 60px; + height: 34px; + font-size: 0; + position: absolute; + top: -52px; + left: -13px; + -webkit-transform: rotate(90deg); + transform: rotate(90deg); +} + +.toggle-all+label:before { + content: '❯'; + font-size: 22px; + color: #e6e6e6; + padding: 10px 27px 10px 27px; +} + +.toggle-all:checked+label:before { + color: #737373; +} + +.todo-list { + margin: 0; + padding: 0; + list-style: none; +} + +.todo-list li { + position: relative; + font-size: 24px; + border-bottom: 1px solid #ededed; +} + +.todo-list li:last-child { + border-bottom: none; +} + +.todo-list li.editing { + border-bottom: none; + padding: 0; +} + +.todo-list li.editing .edit { + display: block; + width: 506px; + padding: 12px 16px; + margin: 0 0 0 43px; +} + +.todo-list li.editing .view { + display: none; +} + +.todo-list li .toggle { + text-align: center; + width: 40px; + /* auto, since non-WebKit browsers doesn't support input styling */ + height: auto; + position: absolute; + top: 0; + bottom: 0; + margin: auto 0; + border: none; + /* Mobile Safari */ + -webkit-appearance: none; + appearance: none; +} + +.todo-list li .toggle { + opacity: 0; +} + +.todo-list li .toggle+label { + /* + Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433 + IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/ + */ + background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E'); + background-repeat: no-repeat; + background-position: center left; +} + +.todo-list li .toggle:checked+label { + background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E'); +} + +.todo-list li label { + word-break: break-all; + padding: 15px 15px 15px 60px; + display: block; + line-height: 1.2; + transition: color 0.4s; +} + +.todo-list li.completed label { + color: #d9d9d9; + text-decoration: line-through; +} + +.todo-list li .destroy { + display: none; + position: absolute; + top: 0; + right: 10px; + bottom: 0; + width: 40px; + height: 40px; + margin: auto 0; + font-size: 30px; + color: #cc9a9a; + margin-bottom: 11px; + transition: color 0.2s ease-out; +} + +.todo-list li .destroy:hover { + color: #af5b5e; +} + +.todo-list li .destroy:after { + content: '×'; +} + +.todo-list li:hover .destroy { + display: block; +} + +.todo-list li .edit { + display: none; +} + +.todo-list li.editing:last-child { + margin-bottom: -1px; +} + +.footer { + color: #777; + padding: 10px 15px; + height: 20px; + text-align: center; + border-top: 1px solid #e6e6e6; +} + +.footer:before { + content: ''; + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 50px; + overflow: hidden; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), + 0 8px 0 -3px #f6f6f6, + 0 9px 1px -3px rgba(0, 0, 0, 0.2), + 0 16px 0 -6px #f6f6f6, + 0 17px 2px -6px rgba(0, 0, 0, 0.2); +} + +.todo-count { + float: left; + text-align: left; +} + +.todo-count strong { + font-weight: 300; +} + +.filters { + margin: 0; + padding: 0; + list-style: none; + position: absolute; + right: 0; + left: 0; +} + +.filters li { + display: inline; +} + +.filters li a { + color: inherit; + margin: 3px; + padding: 3px 7px; + text-decoration: none; + border: 1px solid transparent; + border-radius: 3px; +} + +.filters li a:hover { + border-color: rgba(175, 47, 47, 0.1); +} + +.filters li a.selected { + border-color: rgba(175, 47, 47, 0.2); +} + +.clear-completed, +html .clear-completed:active { + float: right; + position: relative; + line-height: 20px; + text-decoration: none; + cursor: pointer; +} + +.clear-completed:hover { + text-decoration: underline; +} + +.info { + margin: 65px auto 0; + color: #bfbfbf; + font-size: 10px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-align: center; +} + +.info p { + line-height: 1; +} + +.info a { + color: inherit; + text-decoration: none; + font-weight: 400; +} + +.info a:hover { + text-decoration: underline; +} + +/* + Hack to remove background from Mobile Safari. + Can't use it globally since it destroys checkboxes in Firefox +*/ +@media screen and (-webkit-min-device-pixel-ratio:0) { + + .toggle-all, + .todo-list li .toggle { + background: none; + } + + .todo-list li .toggle { + height: 40px; + } +} + +@media (max-width: 430px) { + .footer { + height: 50px; + } + + .filters { + bottom: 10px; + } +}