diff --git a/examples/ios_demo/src/index.html b/examples/ios_demo/src/index.html new file mode 100644 index 000000000..9679d1786 --- /dev/null +++ b/examples/ios_demo/src/index.html @@ -0,0 +1,12 @@ + + + + Dioxus app + + + + +
+ + + diff --git a/examples/ios_demo/src/lib.rs b/examples/ios_demo/src/lib.rs index a45de6c2b..a85282bb7 100644 --- a/examples/ios_demo/src/lib.rs +++ b/examples/ios_demo/src/lib.rs @@ -1,225 +1,44 @@ 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); + // Right now we're going through dioxus-desktop but we'd like to go through dioxus-mobile + // That will seed the index.html with some fixes that prevent the page from scrolling/zooming etc + dioxus_desktop::launch_cfg( + app, + // Note that we have to disable the viewport goofiness of the browser. + // Dioxus_mobile should do this for us + Config::default().with_custom_index(include_str!("index.html").to_string()), + ); + Ok(()) } fn app(cx: Scope) -> Element { - cx.render(rsx! { - div { "Hello World!" } - }) + let items = use_state(cx, || vec![1, 2, 3]); + + render! { + div { + h1 { "Hello, Mobile"} + div { margin_left: "auto", margin_right: "auto", width: "200px", padding: "10px", border: "1px solid black", + button { + onclick: move|_| { + let mut _items = items.make_mut(); + let len = _items.len() + 1; + _items.push(len); + }, + "Add item" + } + for item in items.iter() { + div { "- {item}" } + } + } + } + } } -// #[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( @@ -250,6 +69,10 @@ fn _start_app() { main().unwrap(); } +use dioxus_desktop::Config; +#[cfg(target_os = "android")] +use wry::android_binding; + #[no_mangle] #[inline(never)] #[cfg(any(target_os = "android", target_os = "ios"))] diff --git a/examples/ios_demo/src/style.css b/examples/ios_demo/src/style.css deleted file mode 100644 index ab9dbd7a9..000000000 --- a/examples/ios_demo/src/style.css +++ /dev/null @@ -1,379 +0,0 @@ -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; - } -} diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index fe5c862ed..a8b0fc3ba 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -34,6 +34,7 @@ pub use eval::{use_eval, EvalResult}; use futures_util::{pin_mut, FutureExt}; use shortcut::ShortcutRegistry; pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError}; +use std::cell::Cell; use std::rc::Rc; use std::task::Waker; use std::{collections::HashMap, sync::Arc}; @@ -154,18 +155,10 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) let shortcut_manager = ShortcutRegistry::new(&event_loop); - let web_view = create_new_window( - cfg, - &event_loop, - &proxy, - VirtualDom::new_with_props(root, props), - &queue, - &event_handlers, - shortcut_manager.clone(), - ); - - // By default, we'll create a new window when the app starts - queue.borrow_mut().push(web_view); + // move the props into a cell so we can pop it out later to create the first window + // iOS panics if we create a window before the event loop is started + let props = Rc::new(Cell::new(Some(props))); + let cfg = Rc::new(Cell::new(Some(cfg))); event_loop.run(move |window_event, event_loop, control_flow| { *control_flow = ControlFlow::Wait; @@ -193,8 +186,27 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) _ => {} }, - Event::NewEvents(StartCause::Init) - | Event::UserEvent(UserWindowEvent(EventData::NewWindow, _)) => { + Event::NewEvents(StartCause::Init) => { + // + let props = props.take().unwrap(); + let cfg = cfg.take().unwrap(); + + let handler = create_new_window( + cfg, + &event_loop, + &proxy, + VirtualDom::new_with_props(root, props), + &queue, + &event_handlers, + shortcut_manager.clone(), + ); + + let id = handler.desktop_context.webview.window().id(); + webviews.insert(id, handler); + _ = proxy.send_event(UserWindowEvent(EventData::Poll, id)); + } + + Event::UserEvent(UserWindowEvent(EventData::NewWindow, _)) => { for handler in queue.borrow_mut().drain(..) { let id = handler.desktop_context.webview.window().id(); webviews.insert(id, handler);