Merge branch 'master' into jk/partialexpansion

This commit is contained in:
Jonathan Kelley 2022-02-18 15:31:49 -05:00
commit b2472e3cfe
35 changed files with 503 additions and 417 deletions

View file

@ -37,6 +37,12 @@ web = ["dioxus-web"]
desktop = ["dioxus-desktop"]
router = ["dioxus-router"]
devtool = ["dioxus-desktop/devtool"]
fullscreen = ["dioxus-desktop/fullscreen"]
transparent = ["dioxus-desktop/transparent"]
tray = ["dioxus-desktop/tray"]
ayatana = ["dioxus-desktop/ayatana"]
# "dioxus-router/web"
# "dioxus-router/desktop"
@ -60,15 +66,15 @@ members = [
]
[dev-dependencies]
futures-util = "0.3.17"
futures-util = "0.3.21"
log = "0.4.14"
num-format = "0.4.0"
separator = "0.4.1"
serde = { version = "1.0.131", features = ["derive"] }
serde = { version = "1.0.136", features = ["derive"] }
im-rc = "15.0.0"
anyhow = "1.0.51"
serde_json = "1.0.73"
anyhow = "1.0.53"
serde_json = "1.0.79"
rand = { version = "0.8.4", features = ["small_rng"] }
tokio = { version = "1.14.0", features = ["full"] }
reqwest = { version = "0.11.8", features = ["json"] }
tokio = { version = "1.16.1", features = ["full"] }
reqwest = { version = "0.11.9", features = ["json"] }
dioxus = { path = ".", features = ["desktop", "ssr", "router"] }

View file

@ -101,10 +101,10 @@ cargo run --example EXAMPLE
<table style="width:100%" align="center">
<tr >
<th><a href="https://dioxuslabs.com/guide/">Tutorial</a></th>
<th><a href="https://dioxuslabs.com/reference/web">Web</a></th>
<th><a href="https://dioxuslabs.com/reference/desktop/">Desktop</a></th>
<th><a href="https://dioxuslabs.com/reference/ssr/">SSR</a></th>
<th><a href="https://dioxuslabs.com/reference/mobile/">Mobile</a></th>
<th><a href="https://dioxuslabs.com/reference/platforms/web">Web</a></th>
<th><a href="https://dioxuslabs.com/reference/platforms/desktop/">Desktop</a></th>
<th><a href="https://dioxuslabs.com/reference/platforms/ssr/">SSR</a></th>
<th><a href="https://dioxuslabs.com/reference/platforms/mobile/">Mobile</a></th>
<th><a href="https://dioxuslabs.com/guide/concepts/managing_state.html">State</a></th>
<tr>
</table>

View file

@ -18,7 +18,7 @@ fn app(cx: Scope) -> Element {
In general, Dioxus and React share many functional similarities. If this guide is lacking in any general concept or an error message is confusing, React's documentation might be more helpful. We are dedicated to providing a *familiar* toolkit for UI in Rust, so we've chosen to follow in the footsteps of popular UI frameworks (React, Redux, etc). If you know React, then you already know Dioxus. If you don't know either, this guide will still help you!
> This is an introduction book! For advanced topics, check out the [Reference](https://dioxuslabs.com/reference) instead.
> This is an introduction book! For advanced topics, check out the [Reference](/reference) instead.
## Multiplatform
@ -37,7 +37,7 @@ The Web is the best-supported target platform for Dioxus. To run on the Web, you
Because the web is a fairly mature platform, we expect there to be very little API churn for web-based features.
[Jump to the getting started guide for the web.]()
[Jump to the getting started guide for the web.](/reference/platforms/web)
Examples:
- [TodoMVC](https://github.com/DioxusLabs/example-projects/tree/master/todomvc)
@ -55,7 +55,7 @@ For rendering statically to an `.html` file or from a WebServer, then you'll wan
let contents = dioxus::ssr::render_vdom(&dom);
```
[Jump to the getting started guide for SSR.]()
[Jump to the getting started guide for SSR.](/reference/platforms/ssr)
Examples:
- [Example DocSite](https://github.com/dioxusLabs/docsite)
@ -68,13 +68,13 @@ The desktop is a powerful target for Dioxus, but is currently limited in capabil
Desktop APIs will likely be in flux as we figure out better patterns than our ElectronJS counterpart.
[Jump to the getting started guide for Desktop.]()
[Jump to the getting started guide for Desktop.](/reference/platforms/desktop)
Examples:
- [File explorer](https://github.com/dioxusLabs/file-explorer/)
- [WiFi scanner](https://github.com/DioxusLabs/example-projects/blob/master/wifi-scanner)
[![File ExplorerExample](https://github.com/DioxusLabs/file-explorer-example/raw/master/image.png)](https://github.com/dioxusLabs/file-explorer/)
[![File ExplorerExample](https://raw.githubusercontent.com/DioxusLabs/example-projects/master/file-explorer/image.png)](https://github.com/DioxusLabs/example-projects/tree/master/file-explorer)
### Mobile Support
---
@ -82,7 +82,7 @@ Mobile is currently the least-supported renderer target for Dioxus. Mobile apps
Mobile support is currently best suited for CRUD-style apps, ideally for internal teams who need to develop quickly but don't care much about animations or native widgets.
[Jump to the getting started guide for Mobile.]()
[Jump to the getting started guide for Mobile.](/reference/platforms/mobile)
Examples:
- [Todo App](https://github.com/DioxusLabs/example-projects/blob/master/ios_demo)

View file

@ -140,7 +140,7 @@ fn Child(cx: Scope, name: String) -> Element {
// ✅ Or, use a hashmap with use_ref
```rust
let ages = use_ref(&cx, |_| HashMap::new());
let ages = use_ref(&cx, || HashMap::new());
names.iter().map(|name| {
let age = ages.get(name).unwrap();

View file

@ -8,7 +8,7 @@ We'll learn about:
- Suggested cargo extensions
For platform-specific guides, check out the [Platform Specific Guides](../platforms/00-index.md).
For platform-specific guides, check out the [Platform Specific Guides](/reference/platforms/index.md).
# Setting up Dioxus

View file

@ -2,11 +2,12 @@
- [Introduction](README.md)
- [Web](web/index.md)
- [Desktop](desktop/index.md)
- [Mobile](mobile/index.md)
- [SSR](ssr/index.md)
- [TUI](tui/index.md)
- [Platforms](platforms/index.md)
- [Web](platforms/web.md)
- [Server Side Rendering](platforms/ssr.md)
- [Desktop](platforms/desktop.md)
- [Mobile](platforms/mobile.md)
- [TUI](platforms/tui.md)
- [Advanced Guides](guide/index.md)
- [RSX in Depth](guide/rsx_in_depth.md)

View file

@ -1,4 +1,4 @@
# Desktop
# Getting Started: Desktop
One of Dioxus' killer features is the ability to quickly build a native desktop app that looks and feels the same across platforms. Apps built with Dioxus are typically <5mb in size and use existing system resources, so they won't hog extreme amounts of RAM or memory.

View file

@ -0,0 +1,10 @@
# Platforms
Dioxus supports many different platforms. Below are a list of guides that walk you through setting up Dioxus for a specific platform.
### Setup Guides
- [Web](web.md)
- [Server Side Rendering](ssr.md)
- [Desktop](desktop.md)
- [Mobile](mobile.md)
- [TUI](tui.md)

View file

@ -21,7 +21,7 @@ $ cargo install --git https://github.com/BrainiumLLC/cargo-mobile
And then initialize your app for the right platform. Use the `winit` template for now. Right now, there's no "Dioxus" template in cargo-mobile.
```shell
$ cargo moble init
$ cargo mobile init
```
We're going to completely clear out the `dependencies` it generates for us, swapping out `winit` with `dioxus-mobile`.

View file

@ -1,4 +1,4 @@
# TUI
# Getting Started: TUI
TUI support is currently quite experimental. Even the project name will change. But, if you're willing to venture into the realm of the unknown, this guide will get you started.

View file

@ -1,4 +1,4 @@
# Getting Started: Dioxus for Web
# Getting Started: Web
[*"Pack your things, we're going on an adventure!"*](https://trunkrs.dev)

48
examples/login_form.rs Normal file
View file

@ -0,0 +1,48 @@
//! This example demonstrates the following:
//! Futures in a callback, Router, and Forms
use dioxus::events::*;
use dioxus::prelude::*;
fn main() {
dioxus::desktop::launch(app);
}
fn app(cx: Scope) -> Element {
let onsubmit = move |evt: FormEvent| {
cx.spawn(async move {
let resp = reqwest::Client::new()
.post("http://localhost/login")
.form(&[
("username", &evt.values["username"]),
("password", &evt.values["password"]),
])
.send()
.await;
match resp {
// Parse data from here, such as storing a response token
Ok(_data) => println!("Login successful"),
//Handle any errors from the fetch here
Err(_err) => println!("Login failed"),
}
});
};
cx.render(rsx! {
h1 { "Login" }
form {
onsubmit: onsubmit,
prevent_default: "onsubmit", // Prevent the default behavior of <form> to post
input { "type": "text" }
label { "Username" }
br {}
input { "type": "password" }
label { "Password" }
br {}
button { "Login" }
}
})
}

View file

@ -16,7 +16,6 @@ proc-macro = true
[dependencies]
dioxus-macro-inner = { path = "../macro-inner" }
once_cell = "1.8"
proc-macro-error = "1.0.4"
proc-macro2 = { version = "1.0.6" }
quote = "1.0"

View file

@ -32,7 +32,7 @@ smallvec = "1.6"
slab = "0.4"
futures-channel = "0.3"
futures-channel = "0.3.21"
# used for noderefs
once_cell = "1.8"

View file

@ -13,21 +13,18 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
[dependencies]
dioxus-core = { path = "../core", version = "^0.1.9", features = ["serialize"] }
argh = "0.1.4"
serde = "1.0.120"
serde_json = "1.0.61"
thiserror = "1.0.23"
log = "0.4.13"
html-escape = "0.2.9"
wry = "0.12.2"
futures-channel = "0.3"
tokio = { version = "1.12.0", features = [
serde = "1.0.136"
serde_json = "1.0.79"
thiserror = "1.0.30"
log = "0.4.14"
wry = { version = "0.13.1" }
futures-channel = "0.3.21"
tokio = { version = "1.16.1", features = [
"sync",
"rt-multi-thread",
"rt",
"time",
], optional = true, default-features = false }
dioxus-core-macro = { path = "../core-macro", version = "^0.1.7" }
dioxus-html = { path = "../html", features = ["serialize"], version = "^0.1.6" }
webbrowser = "0.5.5"
mime_guess = "2.0.3"
@ -38,7 +35,15 @@ dioxus-interpreter-js = { path = "../interpreter", version = "^0.0.0" }
default = ["tokio_runtime"]
tokio_runtime = ["tokio"]
devtool = ["wry/devtool"]
fullscreen = ["wry/fullscreen"]
transparent = ["wry/transparent"]
tray = ["wry/tray"]
ayatana = ["wry/ayatana"]
[dev-dependencies]
dioxus-core-macro = { path = "../core-macro", version = "^0.1.7" }
dioxus-hooks = { path = "../hooks" }
# image = "0.24.0" # enable this when generating a new desktop image

View file

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

View file

@ -12,14 +12,14 @@ use wry::{
pub(crate) type DynEventHandlerFn = dyn Fn(&mut EventLoop<()>, &mut WebView);
pub struct DesktopConfig {
pub window: WindowBuilder,
pub file_drop_handler: Option<Box<dyn Fn(&Window, FileDropEvent) -> bool>>,
pub protocols: Vec<WryProtocol>,
pub(crate) window: WindowBuilder,
pub(crate) file_drop_handler: Option<Box<dyn Fn(&Window, FileDropEvent) -> bool>>,
pub(crate) protocols: Vec<WryProtocol>,
pub(crate) pre_rendered: Option<String>,
pub(crate) event_handler: Option<Box<DynEventHandlerFn>>,
}
pub type WryProtocol = (
pub(crate) type WryProtocol = (
String,
Box<dyn Fn(&HttpRequest) -> WryResult<HttpResponse> + 'static>,
);
@ -88,7 +88,7 @@ impl DesktopConfig {
impl DesktopConfig {
pub(crate) fn with_default_icon(mut self) -> Self {
let bin: &[u8] = include_bytes!("default_icon.bin");
let bin: &[u8] = include_bytes!("./assets/default_icon.bin");
let rgba = Icon::from_rgba(bin.to_owned(), 460, 460).expect("image parse failed");
self.window.window.window_icon = Some(rgba);
self

View file

@ -0,0 +1,104 @@
use crate::desktop_context::DesktopContext;
use crate::user_window_events::UserWindowEvent;
use dioxus_core::*;
use std::{
collections::{HashMap, VecDeque},
sync::atomic::AtomicBool,
sync::{Arc, RwLock},
};
use wry::{
self,
application::{event_loop::ControlFlow, event_loop::EventLoopProxy, window::WindowId},
webview::WebView,
};
pub(super) struct DesktopController {
pub(super) webviews: HashMap<WindowId, WebView>,
pub(super) sender: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
pub(super) pending_edits: Arc<RwLock<VecDeque<String>>>,
pub(super) quit_app_on_close: bool,
pub(super) is_ready: Arc<AtomicBool>,
}
impl DesktopController {
// Launch the virtualdom on its own thread managed by tokio
// returns the desktop state
pub(super) fn new_on_tokio<P: Send + 'static>(
root: Component<P>,
props: P,
proxy: EventLoopProxy<UserWindowEvent>,
) -> Self {
let edit_queue = Arc::new(RwLock::new(VecDeque::new()));
let pending_edits = edit_queue.clone();
let (sender, receiver) = futures_channel::mpsc::unbounded::<SchedulerMsg>();
let return_sender = sender.clone();
let desktop_context_proxy = proxy.clone();
std::thread::spawn(move || {
// We create the runtime as multithreaded, so you can still "spawn" onto multiple threads
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap();
runtime.block_on(async move {
let mut dom =
VirtualDom::new_with_props_and_scheduler(root, props, (sender, receiver));
let window_context = DesktopContext::new(desktop_context_proxy);
dom.base_scope().provide_context(window_context);
let edits = dom.rebuild();
edit_queue
.write()
.unwrap()
.push_front(serde_json::to_string(&edits.edits).unwrap());
loop {
dom.wait_for_work().await;
let mut muts = dom.work_with_deadline(|| false);
while let Some(edit) = muts.pop() {
edit_queue
.write()
.unwrap()
.push_front(serde_json::to_string(&edit.edits).unwrap());
}
let _ = proxy.send_event(UserWindowEvent::Update);
}
})
});
Self {
pending_edits,
sender: return_sender,
webviews: HashMap::new(),
is_ready: Arc::new(AtomicBool::new(false)),
quit_app_on_close: true,
}
}
pub(super) fn close_window(&mut self, window_id: WindowId, control_flow: &mut ControlFlow) {
self.webviews.remove(&window_id);
if self.webviews.is_empty() && self.quit_app_on_close {
*control_flow = ControlFlow::Exit;
}
}
pub(super) fn try_load_ready_webviews(&mut self) {
if self.is_ready.load(std::sync::atomic::Ordering::Relaxed) {
let mut queue = self.pending_edits.write().unwrap();
let (_id, view) = self.webviews.iter_mut().next().unwrap();
while let Some(edit) = queue.pop_back() {
view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit))
.unwrap();
}
}
}
}

View file

@ -3,7 +3,8 @@ use std::rc::Rc;
use dioxus_core::ScopeState;
use wry::application::event_loop::EventLoopProxy;
use crate::UserWindowEvent;
use crate::user_window_events::UserWindowEvent;
use UserWindowEvent::*;
type ProxyType = EventLoopProxy<UserWindowEvent>;
@ -35,75 +36,77 @@ impl DesktopContext {
/// onmousedown: move |_| { desktop.drag_window(); }
/// ```
pub fn drag(&self) {
let _ = self.proxy.send_event(UserWindowEvent::DragWindow);
let _ = self.proxy.send_event(DragWindow);
}
/// set window minimize state
pub fn set_minimized(&self, minimized: bool) {
let _ = self.proxy.send_event(UserWindowEvent::Minimize(minimized));
let _ = self.proxy.send_event(Minimize(minimized));
}
/// set window maximize state
pub fn set_maximized(&self, maximized: bool) {
let _ = self.proxy.send_event(UserWindowEvent::Maximize(maximized));
let _ = self.proxy.send_event(Maximize(maximized));
}
/// toggle window maximize state
pub fn toggle_maximized(&self) {
let _ = self.proxy.send_event(MaximizeToggle);
}
/// set window visible or not
pub fn set_visible(&self, visible: bool) {
let _ = self.proxy.send_event(UserWindowEvent::Visible(visible));
let _ = self.proxy.send_event(Visible(visible));
}
/// close window
pub fn close(&self) {
let _ = self.proxy.send_event(UserWindowEvent::CloseWindow);
let _ = self.proxy.send_event(CloseWindow);
}
/// set window to focus
pub fn focus(&self) {
let _ = self.proxy.send_event(UserWindowEvent::FocusWindow);
let _ = self.proxy.send_event(FocusWindow);
}
/// change window to fullscreen
pub fn set_fullscreen(&self, fullscreen: bool) {
let _ = self
.proxy
.send_event(UserWindowEvent::Fullscreen(fullscreen));
let _ = self.proxy.send_event(Fullscreen(fullscreen));
}
/// set resizable state
pub fn set_resizable(&self, resizable: bool) {
let _ = self.proxy.send_event(UserWindowEvent::Resizable(resizable));
let _ = self.proxy.send_event(Resizable(resizable));
}
/// set the window always on top
pub fn set_always_on_top(&self, top: bool) {
let _ = self.proxy.send_event(UserWindowEvent::AlwaysOnTop(top));
let _ = self.proxy.send_event(AlwaysOnTop(top));
}
// set cursor visible or not
pub fn set_cursor_visible(&self, visible: bool) {
let _ = self
.proxy
.send_event(UserWindowEvent::CursorVisible(visible));
let _ = self.proxy.send_event(CursorVisible(visible));
}
// set cursor grab
pub fn set_cursor_grab(&self, grab: bool) {
let _ = self.proxy.send_event(UserWindowEvent::CursorGrab(grab));
let _ = self.proxy.send_event(CursorGrab(grab));
}
/// set window title
pub fn set_title(&self, title: &str) {
let _ = self
.proxy
.send_event(UserWindowEvent::SetTitle(String::from(title)));
let _ = self.proxy.send_event(SetTitle(String::from(title)));
}
/// change window to borderless
pub fn set_decorations(&self, decoration: bool) {
let _ = self
.proxy
.send_event(UserWindowEvent::SetDecorations(decoration));
let _ = self.proxy.send_event(SetDecorations(decoration));
}
/// opens DevTool window
pub fn devtool(&self) {
let _ = self.proxy.send_event(DevTool);
}
}

View file

@ -1,5 +1,4 @@
//! Convert a serialized event to an event Trigger
//!
//! Convert a serialized event to an event trigger
use std::any::Any;
use std::sync::Arc;
@ -7,27 +6,49 @@ use std::sync::Arc;
use dioxus_core::{ElementId, EventPriority, UserEvent};
use dioxus_html::on::*;
#[derive(serde::Serialize, serde::Deserialize)]
pub(crate) struct IpcMessage {
method: String,
params: serde_json::Value,
}
impl IpcMessage {
pub(crate) fn method(&self) -> &str {
self.method.as_str()
}
pub(crate) fn params(self) -> serde_json::Value {
self.params
}
}
pub(crate) fn parse_ipc_message(payload: &str) -> Option<IpcMessage> {
match serde_json::from_str(payload) {
Ok(message) => Some(message),
Err(e) => {
log::error!("could not parse IPC message, error: {e}");
None
}
}
}
#[derive(serde::Serialize, serde::Deserialize)]
struct ImEvent {
event: String,
mounted_dom_id: u64,
// scope: u64,
contents: serde_json::Value,
}
pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent {
let ims: Vec<ImEvent> = serde_json::from_value(val).unwrap();
let ImEvent {
event,
mounted_dom_id,
contents,
} = ims.into_iter().next().unwrap();
} = serde_json::from_value(val).unwrap();
// let scope_id = ScopeId(scope as usize);
let mounted_dom_id = Some(ElementId(mounted_dom_id as usize));
let name = event_name_from_typ(&event);
let name = event_name_from_type(&event);
let event = make_synthetic_event(&event, contents);
UserEvent {
@ -105,7 +126,7 @@ fn make_synthetic_event(name: &str, val: serde_json::Value) -> Arc<dyn Any + Sen
}
}
fn event_name_from_typ(typ: &str) -> &'static str {
fn event_name_from_type(typ: &str) -> &'static str {
match typ {
"copy" => "copy",
"cut" => "cut",

View file

@ -3,31 +3,28 @@
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
pub mod cfg;
mod controller;
pub mod desktop_context;
pub mod escape;
pub mod events;
mod protocol;
mod user_window_events;
use cfg::DesktopConfig;
use controller::DesktopController;
pub use desktop_context::use_window;
use desktop_context::DesktopContext;
use dioxus_core::*;
use std::{
collections::{HashMap, VecDeque},
sync::atomic::AtomicBool,
sync::{Arc, RwLock},
};
use events::parse_ipc_message;
use tao::{
event::{Event, StartCause, WindowEvent},
event_loop::{ControlFlow, EventLoop},
window::{Window, WindowId},
window::Window,
};
pub use wry;
pub use wry::application as tao;
use wry::{
application::{event_loop::EventLoopProxy, window::Fullscreen},
webview::RpcRequest,
webview::{WebView, WebViewBuilder},
};
use wry::webview::WebViewBuilder;
use crate::events::trigger_from_serialized;
/// Launch the WebView and run the event loop.
///
@ -132,23 +129,24 @@ pub fn launch_with_props<P: 'static + Send>(
.with_transparent(cfg.window.window.transparent)
.with_url("dioxus://index.html/")
.unwrap()
.with_rpc_handler(move |_window: &Window, req: RpcRequest| {
match req.method.as_str() {
"user_event" => {
let event = events::trigger_from_serialized(req.params.unwrap());
log::trace!("User event: {:?}", event);
sender.unbounded_send(SchedulerMsg::Event(event)).unwrap();
}
"initialize" => {
is_ready.store(true, std::sync::atomic::Ordering::Relaxed);
let _ = proxy.send_event(UserWindowEvent::Update);
}
"browser_open" => {
println!("browser_open");
let data = req.params.unwrap();
log::trace!("Open browser: {:?}", data);
if let Some(arr) = data.as_array() {
if let Some(temp) = arr[0].as_object() {
.with_ipc_handler(move |_window: &Window, payload: String| {
parse_ipc_message(&payload)
.map(|message| match message.method() {
"user_event" => {
let event = trigger_from_serialized(message.params());
log::trace!("User event: {:?}", event);
sender.unbounded_send(SchedulerMsg::Event(event)).unwrap();
}
"initialize" => {
is_ready.store(true, std::sync::atomic::Ordering::Relaxed);
let _ = proxy
.send_event(user_window_events::UserWindowEvent::Update);
}
"browser_open" => {
println!("browser_open");
let data = message.params();
log::trace!("Open browser: {:?}", data);
if let Some(temp) = data.as_object() {
if temp.contains_key("href") {
let url = temp.get("href").unwrap().as_str().unwrap();
if let Err(e) = webbrowser::open(url) {
@ -157,55 +155,13 @@ pub fn launch_with_props<P: 'static + Send>(
}
}
}
}
_ => {}
}
None
})
.with_custom_protocol(String::from("dioxus"), move |request| {
// Any content that that uses the `dioxus://` scheme will be shuttled through this handler as a "special case"
// For now, we only serve two pieces of content which get included as bytes into the final binary.
let path = request.uri().replace("dioxus://", "");
// all assets shouldbe called from index.html
let trimmed = path.trim_start_matches("index.html/");
if trimmed.is_empty() {
wry::http::ResponseBuilder::new()
.mimetype("text/html")
.body(include_bytes!("./index.html").to_vec())
} else if trimmed == "index.js" {
wry::http::ResponseBuilder::new()
.mimetype("text/javascript")
.body(dioxus_interpreter_js::INTERPRTER_JS.as_bytes().to_vec())
} else {
// Read the file content from file path
use std::fs::read;
let path_buf = std::path::Path::new(trimmed).canonicalize()?;
let cur_path = std::path::Path::new(".").canonicalize()?;
if !path_buf.starts_with(cur_path) {
return wry::http::ResponseBuilder::new()
.status(wry::http::status::StatusCode::FORBIDDEN)
.body(String::from("Forbidden").into_bytes());
}
if !path_buf.exists() {
return wry::http::ResponseBuilder::new()
.status(wry::http::status::StatusCode::NOT_FOUND)
.body(String::from("Not Found").into_bytes());
}
let mime = mime_guess::from_path(&path_buf).first_or_octet_stream();
// do not let path searching to go two layers beyond the caller level
let data = read(path_buf)?;
let meta = format!("{}", mime);
wry::http::ResponseBuilder::new().mimetype(&meta).body(data)
}
_ => (),
})
.unwrap_or_else(|| {
log::warn!("invalid IPC message received");
});
})
.with_custom_protocol(String::from("dioxus"), protocol::desktop_handler)
.with_file_drop_handler(move |window, evet| {
file_handler
.as_ref()
@ -235,114 +191,8 @@ pub fn launch_with_props<P: 'static + Send>(
_ => {}
},
Event::UserEvent(_evt) => {
//
match _evt {
UserWindowEvent::Update => desktop.try_load_ready_webviews(),
UserWindowEvent::DragWindow => {
// this loop just run once, because dioxus-desktop is unsupport multi-window.
for webview in desktop.webviews.values() {
let window = webview.window();
// start to drag the window.
// if the drag_window have any err. we don't do anything.
if window.fullscreen().is_some() {
return;
}
let _ = window.drag_window();
}
}
UserWindowEvent::CloseWindow => {
// close window
*control_flow = ControlFlow::Exit;
}
UserWindowEvent::Visible(state) => {
for webview in desktop.webviews.values() {
let window = webview.window();
window.set_visible(state);
}
}
UserWindowEvent::Minimize(state) => {
// this loop just run once, because dioxus-desktop is unsupport multi-window.
for webview in desktop.webviews.values() {
let window = webview.window();
// change window minimized state.
window.set_minimized(state);
}
}
UserWindowEvent::Maximize(state) => {
// this loop just run once, because dioxus-desktop is unsupport multi-window.
for webview in desktop.webviews.values() {
let window = webview.window();
// change window maximized state.
window.set_maximized(state);
}
}
UserWindowEvent::Fullscreen(state) => {
for webview in desktop.webviews.values() {
let window = webview.window();
let current_monitor = window.current_monitor();
if current_monitor.is_none() {
return;
}
let fullscreen = if state {
Some(Fullscreen::Borderless(current_monitor))
} else {
None
};
window.set_fullscreen(fullscreen);
}
}
UserWindowEvent::FocusWindow => {
for webview in desktop.webviews.values() {
let window = webview.window();
window.set_focus();
}
}
UserWindowEvent::Resizable(state) => {
for webview in desktop.webviews.values() {
let window = webview.window();
window.set_resizable(state);
}
}
UserWindowEvent::AlwaysOnTop(state) => {
for webview in desktop.webviews.values() {
let window = webview.window();
window.set_always_on_top(state);
}
}
UserWindowEvent::CursorVisible(state) => {
for webview in desktop.webviews.values() {
let window = webview.window();
window.set_cursor_visible(state);
}
}
UserWindowEvent::CursorGrab(state) => {
for webview in desktop.webviews.values() {
let window = webview.window();
let _ = window.set_cursor_grab(state);
}
}
UserWindowEvent::SetTitle(content) => {
for webview in desktop.webviews.values() {
let window = webview.window();
window.set_title(&content);
}
}
UserWindowEvent::SetDecorations(state) => {
for webview in desktop.webviews.values() {
let window = webview.window();
window.set_decorations(state);
}
}
}
Event::UserEvent(user_event) => {
user_window_events::handler(user_event, &mut desktop, control_flow)
}
Event::MainEventsCleared => {}
Event::Resumed => {}
@ -353,118 +203,3 @@ pub fn launch_with_props<P: 'static + Send>(
}
})
}
pub enum UserWindowEvent {
Update,
DragWindow,
CloseWindow,
FocusWindow,
Visible(bool),
Minimize(bool),
Maximize(bool),
Resizable(bool),
AlwaysOnTop(bool),
Fullscreen(bool),
CursorVisible(bool),
CursorGrab(bool),
SetTitle(String),
SetDecorations(bool),
}
pub struct DesktopController {
pub proxy: EventLoopProxy<UserWindowEvent>,
pub webviews: HashMap<WindowId, WebView>,
pub sender: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
pub pending_edits: Arc<RwLock<VecDeque<String>>>,
pub quit_app_on_close: bool,
pub is_ready: Arc<AtomicBool>,
}
impl DesktopController {
// Launch the virtualdom on its own thread managed by tokio
// returns the desktop state
pub fn new_on_tokio<P: Send + 'static>(
root: Component<P>,
props: P,
evt: EventLoopProxy<UserWindowEvent>,
) -> Self {
let edit_queue = Arc::new(RwLock::new(VecDeque::new()));
let pending_edits = edit_queue.clone();
let (sender, receiver) = futures_channel::mpsc::unbounded::<SchedulerMsg>();
let return_sender = sender.clone();
let proxy = evt.clone();
let desktop_context_proxy = proxy.clone();
std::thread::spawn(move || {
// We create the runtime as multithreaded, so you can still "spawn" onto multiple threads
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap();
runtime.block_on(async move {
let mut dom =
VirtualDom::new_with_props_and_scheduler(root, props, (sender, receiver));
let window_context = DesktopContext::new(desktop_context_proxy);
dom.base_scope().provide_context(window_context);
let edits = dom.rebuild();
edit_queue
.write()
.unwrap()
.push_front(serde_json::to_string(&edits.edits).unwrap());
loop {
dom.wait_for_work().await;
let mut muts = dom.work_with_deadline(|| false);
while let Some(edit) = muts.pop() {
edit_queue
.write()
.unwrap()
.push_front(serde_json::to_string(&edit.edits).unwrap());
}
let _ = evt.send_event(UserWindowEvent::Update);
}
})
});
Self {
pending_edits,
sender: return_sender,
proxy,
webviews: HashMap::new(),
is_ready: Arc::new(AtomicBool::new(false)),
quit_app_on_close: true,
}
}
pub fn close_window(&mut self, window_id: WindowId, control_flow: &mut ControlFlow) {
self.webviews.remove(&window_id);
if self.webviews.is_empty() && self.quit_app_on_close {
*control_flow = ControlFlow::Exit;
}
}
pub fn try_load_ready_webviews(&mut self) {
if self.is_ready.load(std::sync::atomic::Ordering::Relaxed) {
let mut queue = self.pending_edits.write().unwrap();
let (_id, view) = self.webviews.iter_mut().next().unwrap();
while let Some(edit) = queue.pop_back() {
view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit))
.unwrap();
}
} else {
println!("waiting for ready");
}
}
}

View file

@ -0,0 +1,47 @@
use std::path::Path;
use wry::{
http::{status::StatusCode, Request, Response, ResponseBuilder},
Result,
};
pub(super) fn desktop_handler(request: &Request) -> Result<Response> {
// Any content that uses the `dioxus://` scheme will be shuttled through this handler as a "special case".
// For now, we only serve two pieces of content which get included as bytes into the final binary.
let path = request.uri().replace("dioxus://", "");
// all assets should be called from index.html
let trimmed = path.trim_start_matches("index.html/");
if trimmed.is_empty() {
ResponseBuilder::new()
.mimetype("text/html")
.body(include_bytes!("./index.html").to_vec())
} else if trimmed == "index.js" {
ResponseBuilder::new()
.mimetype("text/javascript")
.body(dioxus_interpreter_js::INTERPRETER_JS.as_bytes().to_vec())
} else {
let path_buf = Path::new(trimmed).canonicalize()?;
let cur_path = Path::new(".").canonicalize()?;
if !path_buf.starts_with(cur_path) {
return ResponseBuilder::new()
.status(StatusCode::FORBIDDEN)
.body(String::from("Forbidden").into_bytes());
}
if !path_buf.exists() {
return ResponseBuilder::new()
.status(StatusCode::NOT_FOUND)
.body(String::from("Not Found").into_bytes());
}
let mime = mime_guess::from_path(&path_buf).first_or_octet_stream();
// do not let path searching to go two layers beyond the caller level
let data = std::fs::read(path_buf)?;
let meta = format!("{}", mime);
ResponseBuilder::new().mimetype(&meta).body(data)
}
}

View file

@ -0,0 +1,72 @@
use wry::application::event_loop::ControlFlow;
use wry::application::window::Fullscreen as WryFullscreen;
use crate::controller::DesktopController;
pub(crate) enum UserWindowEvent {
Update,
CloseWindow,
DragWindow,
FocusWindow,
Visible(bool),
Minimize(bool),
Maximize(bool),
MaximizeToggle,
Resizable(bool),
AlwaysOnTop(bool),
Fullscreen(bool),
CursorVisible(bool),
CursorGrab(bool),
SetTitle(String),
SetDecorations(bool),
DevTool,
}
use UserWindowEvent::*;
pub(super) fn handler(
user_event: UserWindowEvent,
desktop: &mut DesktopController,
control_flow: &mut ControlFlow,
) {
// currently dioxus-desktop supports a single window only,
// so we can grab the only webview from the map;
let webview = desktop.webviews.values().next().unwrap();
let window = webview.window();
match user_event {
Update => desktop.try_load_ready_webviews(),
CloseWindow => *control_flow = ControlFlow::Exit,
DragWindow => {
// if the drag_window has any errors, we don't do anything
window.fullscreen().is_none().then(|| window.drag_window());
}
Visible(state) => window.set_visible(state),
Minimize(state) => window.set_minimized(state),
Maximize(state) => window.set_maximized(state),
MaximizeToggle => window.set_maximized(!window.is_maximized()),
Fullscreen(state) => {
if let Some(handle) = window.current_monitor() {
window.set_fullscreen(state.then(|| WryFullscreen::Borderless(Some(handle))));
}
}
FocusWindow => window.set_focus(),
Resizable(state) => window.set_resizable(state),
AlwaysOnTop(state) => window.set_always_on_top(state),
CursorVisible(state) => window.set_cursor_visible(state),
CursorGrab(state) => {
let _ = window.set_cursor_grab(state);
}
SetTitle(content) => window.set_title(&content),
SetDecorations(state) => window.set_decorations(state),
DevTool => webview.devtool(),
}
}

View file

@ -2,7 +2,7 @@ export function main() {
let root = window.document.getElementById("main");
if (root != null) {
window.interpreter = new Interpreter(root);
window.rpc.call("initialize");
window.ipc.postMessage(serializeIpcMessage("initialize"));
}
}
export class Interpreter {
@ -105,7 +105,7 @@ export class Interpreter {
if (ns === "style") {
// @ts-ignore
node.style[name] = value;
} else if (ns != null || ns !== undefined) {
} else if (ns != null || ns != undefined) {
node.setAttributeNS(ns, name, value);
} else {
switch (name) {
@ -207,7 +207,9 @@ export class Interpreter {
event.preventDefault();
const href = target.getAttribute("href");
if (href !== "" && href !== null && href !== undefined) {
window.rpc.call("browser_open", { href });
window.ipc.postMessage(
serializeIpcMessage("browser_open", { href })
);
}
}
}
@ -261,11 +263,13 @@ export class Interpreter {
if (realId == null) {
return;
}
window.rpc.call("user_event", {
event: edit.event_name,
mounted_dom_id: parseInt(realId),
contents: contents,
});
window.ipc.postMessage(
serializeIpcMessage("user_event", {
event: edit.event_name,
mounted_dom_id: parseInt(realId),
contents: contents,
})
);
}
};
this.NewEventListener(edit.event_name, edit.root, handler);
@ -341,6 +345,7 @@ export function serialize_event(event) {
}
return {
value: value,
values: {},
};
}
case "input":
@ -544,6 +549,9 @@ export function serialize_event(event) {
}
}
}
function serializeIpcMessage(method, params = {}) {
return JSON.stringify({ method, params });
}
const bool_attrs = {
allowfullscreen: true,
allowpaymentrequest: true,

View file

@ -1,4 +1,4 @@
pub static INTERPRTER_JS: &str = include_str!("./interpreter.js");
pub static INTERPRETER_JS: &str = include_str!("./interpreter.js");
#[cfg(feature = "web")]
mod bindings;

View file

@ -16,8 +16,8 @@ dioxus-html = { path = "../html", version = "^0.1.6", default-features = false }
dioxus-core-macro = { path = "../core-macro", version = "^0.1.7" }
serde = "1"
url = "2.2.2"
serde_urlencoded = "0.7"
# url = "2.2.2"
# for wasm
web-sys = { version = "0.3", features = [

View file

@ -34,6 +34,17 @@ pub struct LinkProps<'a> {
#[props(default, strip_option)]
title: Option<&'a str>,
#[props(default = true)]
autodetect: bool,
/// Is this link an external link?
#[props(default = false)]
external: bool,
/// New tab?
#[props(default = false)]
new_tab: bool,
children: Element<'a>,
#[props(default)]
@ -41,17 +52,38 @@ pub struct LinkProps<'a> {
}
pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
// log::trace!("render Link to {}", cx.props.to);
if let Some(service) = cx.consume_context::<RouterService>() {
let LinkProps {
to,
href,
class,
id,
title,
autodetect,
external,
new_tab,
children,
..
} = cx.props;
let is_http = to.starts_with("http") || to.starts_with("https");
let outerlink = (*autodetect && is_http) || *external;
let prevent_default = if outerlink { "" } else { "onclick" };
return cx.render(rsx! {
a {
href: "{cx.props.to}",
class: format_args!("{}", cx.props.class.unwrap_or("")),
id: format_args!("{}", cx.props.id.unwrap_or("")),
title: format_args!("{}", cx.props.title.unwrap_or("")),
prevent_default: "onclick",
onclick: move |_| service.push_route(cx.props.to),
href: "{to}",
class: format_args!("{}", class.unwrap_or("")),
id: format_args!("{}", id.unwrap_or("")),
title: format_args!("{}", title.unwrap_or("")),
prevent_default: "{prevent_default}",
target: format_args!("{}", if *new_tab { "_blank" } else { "" }),
onclick: move |_| {
if !outerlink {
service.push_route(to);
}
},
&cx.props.children
}

View file

@ -32,7 +32,5 @@ pub fn Router<'a>(cx: Scope<'a, RouterProps<'a>>) -> Element {
cx.props.onchange.call(path.to_string());
}
cx.render(rsx!(
div { &cx.props.children }
))
cx.render(rsx!(&cx.props.children))
}

View file

@ -106,7 +106,7 @@ impl Drop for UseRouteListener {
}
/// This hook provides access to the `RouterService` for the app.
pub fn use_router(cx: &ScopeState) -> &RouterService {
pub fn use_router(cx: &ScopeState) -> &Rc<RouterService> {
cx.use_hook(|_| {
cx.consume_context::<RouterService>()
.expect("Cannot call use_route outside the scope of a Router component")

View file

@ -15,21 +15,18 @@ dioxus-core = { path = "../core", version = "^0.1.9" }
dioxus-html = { path = "../html", version = "^0.1.6" }
js-sys = "0.3.56"
wasm-bindgen = { version = "0.2.79", features = ["enable-interning"] }
lazy_static = "1.4.0"
wasm-bindgen-futures = "0.4.29"
log = { version = "0.4.14", features = ["release_max_level_off"] }
fxhash = "0.2.1"
wasm-logger = "0.2.0"
console_error_panic_hook = { version = "0.1.7", optional = true }
wasm-bindgen-test = "0.3.29"
once_cell = "1.9.0"
async-channel = "1.6.1"
anyhow = "1.0.53"
gloo-timers = { version = "0.2.3", features = ["futures"] }
futures-util = "0.3.19"
smallstr = "0.2.0"
dioxus-interpreter-js = { path = "../interpreter", version = "^0.0.0", features = ["web"] }
serde-wasm-bindgen = "0.4.2"
futures-channel = "0.3.21"
[dependencies.web-sys]
version = "0.3.56"
@ -80,3 +77,5 @@ panic_hook = ["console_error_panic_hook"]
dioxus-core-macro = { path = "../core-macro" }
wasm-bindgen-test = "0.3.29"
dioxus-ssr = { path = "../ssr" }
wasm-logger = "0.2.0"

View file

@ -87,14 +87,11 @@ impl WebsysDom {
}
});
// a match here in order to avoid some error during runtime browser test
let document = load_document();
let root = match document.get_element_by_id(&cfg.rootname) {
Some(root) => root,
// a match here in order to avoid some error during runtime browser test
None => {
let body = document.create_element("body").ok().unwrap();
body
}
None => document.create_element("body").ok().unwrap(),
};
Self {

View file

@ -211,7 +211,7 @@ pub async fn run_with_props<T: 'static + Send>(root: Component<T>, root_props: T
websys_dom.apply_edits(edits.edits);
}
let work_loop = ric_raf::RafLoop::new();
let mut work_loop = ric_raf::RafLoop::new();
loop {
log::trace!("waiting for work");

View file

@ -7,6 +7,7 @@
//! Because RIC doesn't work on Safari, we polyfill using the "ricpolyfill.js" file and use some basic detection to see
//! if RIC is available.
use futures_util::StreamExt;
use gloo_timers::future::TimeoutFuture;
use js_sys::Function;
use wasm_bindgen::{prelude::Closure, JsCast, JsValue};
@ -14,21 +15,21 @@ use web_sys::{window, Window};
pub(crate) struct RafLoop {
window: Window,
ric_receiver: async_channel::Receiver<u32>,
raf_receiver: async_channel::Receiver<()>,
ric_receiver: futures_channel::mpsc::UnboundedReceiver<u32>,
raf_receiver: futures_channel::mpsc::UnboundedReceiver<()>,
ric_closure: Closure<dyn Fn(JsValue)>,
raf_closure: Closure<dyn Fn(JsValue)>,
}
impl RafLoop {
pub fn new() -> Self {
let (raf_sender, raf_receiver) = async_channel::unbounded();
let (raf_sender, raf_receiver) = futures_channel::mpsc::unbounded();
let raf_closure: Closure<dyn Fn(JsValue)> = Closure::wrap(Box::new(move |_v: JsValue| {
raf_sender.try_send(()).unwrap()
raf_sender.unbounded_send(()).unwrap()
}));
let (ric_sender, ric_receiver) = async_channel::unbounded();
let (ric_sender, ric_receiver) = futures_channel::mpsc::unbounded();
let has_idle_callback = {
let bo = window().unwrap().dyn_into::<js_sys::Object>().unwrap();
@ -45,7 +46,7 @@ impl RafLoop {
10
};
ric_sender.try_send(time_remaining).unwrap()
ric_sender.unbounded_send(time_remaining).unwrap()
}));
// execute the polyfill for safari
@ -64,16 +65,16 @@ impl RafLoop {
}
}
/// waits for some idle time and returns a timeout future that expires after the idle time has passed
pub async fn wait_for_idle_time(&self) -> TimeoutFuture {
pub async fn wait_for_idle_time(&mut self) -> TimeoutFuture {
let ric_fn = self.ric_closure.as_ref().dyn_ref::<Function>().unwrap();
let _cb_id: u32 = self.window.request_idle_callback(ric_fn).unwrap();
let deadline = self.ric_receiver.recv().await.unwrap();
let deadline = self.ric_receiver.next().await.unwrap();
TimeoutFuture::new(deadline)
}
pub async fn wait_for_raf(&self) {
pub async fn wait_for_raf(&mut self) {
let raf_fn = self.raf_closure.as_ref().dyn_ref::<Function>().unwrap();
let _id: i32 = self.window.request_animation_frame(raf_fn).unwrap();
self.raf_receiver.recv().await.unwrap();
self.raf_receiver.next().await.unwrap();
}
}