Merge branch 'upstream' into fix-non-str-attributes
This commit is contained in:
commit
5b1d9c5c8d
44 files changed
+1335
-2346
No files matched your search
+35
-74
@@ -1,88 +1,49 @@
|
||||
# This crate autofmts blocks of rsx!
|
||||
|
||||
This crate formats rsx! by parsing call bodies and pretty-printing them back out.
|
||||
# dioxus-autofmt
|
||||
|
||||
|
||||
[![Crates.io][crates-badge]][crates-url]
|
||||
[![MIT licensed][mit-badge]][mit-url]
|
||||
[![Build Status][actions-badge]][actions-url]
|
||||
[![Discord chat][discord-badge]][discord-url]
|
||||
|
||||
# Todo:
|
||||
Sorted roughly in order of what's possible
|
||||
[crates-badge]: https://img.shields.io/crates/v/dioxus-autofmt.svg
|
||||
[crates-url]: https://crates.io/crates/dioxus-autofmt
|
||||
|
||||
- [x] Oneline rsx! calls - blocker because this wrecks formatting
|
||||
- [ ] Nested RSX calls (important) - unnecessary but desirable
|
||||
- [ ] RSX edits overstepping each other
|
||||
- [ ] Collapse components and elements under syntax -
|
||||
- [ ] Don't eat comments in exprs
|
||||
- [ ] Format regular exprs
|
||||
- [ ] Fix prettyplease around chaining
|
||||
- [ ] Don't eat comments in prettyplease
|
||||
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||
[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
|
||||
|
||||
[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
|
||||
[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster
|
||||
|
||||
[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square
|
||||
[discord-url]: https://discord.gg/XgGxMSkvUM
|
||||
|
||||
[Website](https://dioxuslabs.com) |
|
||||
[Guides](https://dioxuslabs.com/guide/) |
|
||||
[API Docs](https://docs.rs/dioxus-autofmt/latest/dioxus-autofmt) |
|
||||
[Chat](https://discord.gg/XgGxMSkvUM)
|
||||
|
||||
|
||||
# Technique
|
||||
## Overview
|
||||
|
||||
`dioxus-autofmt` provides a pretty printer for the `rsx` syntax tree.
|
||||
|
||||
|
||||
div {
|
||||
div {}
|
||||
div {}
|
||||
}
|
||||
This is done manually with a via set of formatting rules. The output is not guaranteed to be stable between minor versions of the crate as we might tweak the output.
|
||||
|
||||
`dioxus-autofmt` provides an API to perform precision edits as well as just spit out a block of formatted RSX from any RSX syntax tree. This is used by the `rsx-rosetta` crate which can accept various input languages and output valid RSX.
|
||||
|
||||
|
||||
div
|
||||
## Contributing
|
||||
|
||||
possible line break
|
||||
div
|
||||
div
|
||||
- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
|
||||
- Join the discord and ask questions!
|
||||
|
||||
## License
|
||||
This project is licensed under the [MIT license].
|
||||
|
||||
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
|
||||
|
||||
string of possible items within a nesting
|
||||
div {
|
||||
attr_pair
|
||||
expr
|
||||
text
|
||||
comment
|
||||
}
|
||||
a nesting is either a component or an element
|
||||
|
||||
idea:
|
||||
collect all items into a queue
|
||||
q
|
||||
```rust
|
||||
section {
|
||||
div {
|
||||
h1 { p { "asdasd" } }
|
||||
h1 { p { "asdasd" } }
|
||||
}
|
||||
}
|
||||
|
||||
section {}
|
||||
```
|
||||
|
||||
|
||||
// space
|
||||
// space
|
||||
// space
|
||||
|
||||
|
||||
3 - section
|
||||
3 - section div
|
||||
3 - section div h1
|
||||
3 - section div h1 p
|
||||
3 - section div h1 p text
|
||||
3 - section
|
||||
3 - section div
|
||||
3 - section div h1
|
||||
3 - section div h1 p
|
||||
3 - section div h1 p text
|
||||
|
||||
block
|
||||
|
||||
- when we hit the end of a trail, we can make a decision what needs to be hard breaked
|
||||
- most nestings cannot be merged into a single one, so at some point we need to write the line break
|
||||
- this is the scan section. we scan forward until it's obvious where to place a hard break
|
||||
- when a line is finished, we can print it out by unloading our queued items
|
||||
- never double nested
|
||||
|
||||
|
||||
Terms
|
||||
- break is a whitespace than can flex, dependent on the situation
|
||||
- ‹›
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in Dioxus by you, shall be licensed as MIT, without any additional
|
||||
terms or conditions.
|
||||
@@ -1,3 +1,5 @@
|
||||
use dioxus_rsx::CallBody;
|
||||
|
||||
use crate::buffer::*;
|
||||
use crate::util::*;
|
||||
|
||||
@@ -31,6 +33,11 @@ pub struct FormattedBlock {
|
||||
/// Format a file into a list of `FormattedBlock`s to be applied by an IDE for autoformatting.
|
||||
///
|
||||
/// This function expects a complete file, not just a block of code. To format individual rsx! blocks, use fmt_block instead.
|
||||
///
|
||||
/// The point here is to provide precise modifications of a source file so an accompanying IDE tool can map these changes
|
||||
/// back to the file precisely.
|
||||
///
|
||||
/// Nested blocks of RSX will be handled automatically
|
||||
pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
|
||||
let mut formatted_blocks = Vec::new();
|
||||
let mut last_bracket_end = 0;
|
||||
@@ -93,15 +100,32 @@ pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
|
||||
formatted_blocks
|
||||
}
|
||||
|
||||
pub fn write_block_out(body: CallBody) -> Option<String> {
|
||||
let mut buf = Buffer {
|
||||
src: vec!["".to_string()],
|
||||
indent: 0,
|
||||
..Buffer::default()
|
||||
};
|
||||
|
||||
// Oneliner optimization
|
||||
if buf.is_short_children(&body.roots).is_some() {
|
||||
buf.write_ident(&body.roots[0]).unwrap();
|
||||
} else {
|
||||
buf.write_body_indented(&body.roots).unwrap();
|
||||
}
|
||||
|
||||
buf.consume()
|
||||
}
|
||||
|
||||
pub fn fmt_block(block: &str, indent_level: usize) -> Option<String> {
|
||||
let body = syn::parse_str::<dioxus_rsx::CallBody>(block).ok()?;
|
||||
|
||||
let mut buf = Buffer {
|
||||
src: block.lines().map(|f| f.to_string()).collect(),
|
||||
indent: indent_level,
|
||||
..Buffer::default()
|
||||
};
|
||||
|
||||
let body = syn::parse_str::<dioxus_rsx::CallBody>(block).unwrap();
|
||||
|
||||
// Oneliner optimization
|
||||
if buf.is_short_children(&body.roots).is_some() {
|
||||
buf.write_ident(&body.roots[0]).unwrap();
|
||||
|
||||
@@ -1,7 +1,48 @@
|
||||
# core-macro
|
||||
# dioxus-core-macro
|
||||
|
||||
This crate implements these macros:
|
||||
- `format_args_f`: for f-string interpolation inside of text blocks
|
||||
- `Props`: derive macro for typed-builder with props configurations
|
||||
-
|
||||
-
|
||||
|
||||
[![Crates.io][crates-badge]][crates-url]
|
||||
[![MIT licensed][mit-badge]][mit-url]
|
||||
[![Build Status][actions-badge]][actions-url]
|
||||
[![Discord chat][discord-badge]][discord-url]
|
||||
|
||||
[crates-badge]: https://img.shields.io/crates/v/dioxus-core-macro.svg
|
||||
[crates-url]: https://crates.io/crates/dioxus-core-macro
|
||||
|
||||
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||
[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
|
||||
|
||||
[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
|
||||
[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster
|
||||
|
||||
[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square
|
||||
[discord-url]: https://discord.gg/XgGxMSkvUM
|
||||
|
||||
[Website](https://dioxuslabs.com) |
|
||||
[Guides](https://dioxuslabs.com/guide/) |
|
||||
[API Docs](https://docs.rs/dioxus-core-macro/latest/dioxus-core-macro) |
|
||||
[Chat](https://discord.gg/XgGxMSkvUM)
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
`dioxus-core-macro` provides a handful of helpful macros used by the `dioxus` crate. These include:
|
||||
|
||||
- The `rsx!` macro that underpins templates and node creation
|
||||
- The `inline_props` that transforms function arguments into an auto-derived struct
|
||||
- The `format_args_f` macro which allows f-string formatting with support for expressions
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
|
||||
- Join the discord and ask questions!
|
||||
|
||||
## License
|
||||
This project is licensed under the [MIT license].
|
||||
|
||||
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in Dioxus by you, shall be licensed as MIT, without any additional
|
||||
terms or conditions.
|
||||
@@ -23,7 +23,7 @@ rustc-hash = "1.1.0"
|
||||
# Used in diffing
|
||||
longest-increasing-subsequence = "0.1.0"
|
||||
|
||||
futures-util = { version = "0.3", default-features = false }
|
||||
futures-util = { version = "0.3", default-features = false, features = ["alloc"]}
|
||||
|
||||
slab = "0.4"
|
||||
|
||||
|
||||
@@ -4,11 +4,9 @@ use slab::Slab;
|
||||
mod suspense;
|
||||
mod task;
|
||||
mod wait;
|
||||
mod waker;
|
||||
|
||||
pub use suspense::*;
|
||||
pub use task::*;
|
||||
pub use waker::ArcWake;
|
||||
|
||||
/// The type of message that can be sent to the scheduler.
|
||||
///
|
||||
@@ -25,16 +23,16 @@ pub(crate) enum SchedulerMsg {
|
||||
SuspenseNotified(SuspenseId),
|
||||
}
|
||||
|
||||
use std::{cell::RefCell, rc::Rc, sync::Arc};
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
pub(crate) struct Scheduler {
|
||||
pub sender: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
|
||||
|
||||
/// Tasks created with cx.spawn
|
||||
pub tasks: RefCell<Slab<Arc<LocalTask>>>,
|
||||
pub tasks: RefCell<Slab<LocalTask>>,
|
||||
|
||||
/// Async components
|
||||
pub leaves: RefCell<Slab<Arc<SuspenseLeaf>>>,
|
||||
pub leaves: RefCell<Slab<SuspenseLeaf>>,
|
||||
}
|
||||
|
||||
impl Scheduler {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
use super::{waker::ArcWake, SchedulerMsg};
|
||||
use futures_util::task::ArcWake;
|
||||
|
||||
use super::SchedulerMsg;
|
||||
use crate::ElementId;
|
||||
use crate::{innerlude::Mutations, Element, ScopeId};
|
||||
use std::future::Future;
|
||||
use std::sync::Arc;
|
||||
use std::task::Waker;
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
collections::HashSet,
|
||||
@@ -35,16 +38,19 @@ impl SuspenseContext {
|
||||
}
|
||||
|
||||
pub(crate) struct SuspenseLeaf {
|
||||
pub(crate) id: SuspenseId,
|
||||
pub(crate) scope_id: ScopeId,
|
||||
pub(crate) tx: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
|
||||
pub(crate) notified: Cell<bool>,
|
||||
pub(crate) task: *mut dyn Future<Output = Element<'static>>,
|
||||
pub(crate) waker: Waker,
|
||||
}
|
||||
|
||||
impl ArcWake for SuspenseLeaf {
|
||||
pub struct SuspenseHandle {
|
||||
pub(crate) id: SuspenseId,
|
||||
pub(crate) tx: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
|
||||
}
|
||||
|
||||
impl ArcWake for SuspenseHandle {
|
||||
fn wake_by_ref(arc_self: &Arc<Self>) {
|
||||
arc_self.notified.set(true);
|
||||
_ = arc_self
|
||||
.tx
|
||||
.unbounded_send(SchedulerMsg::SuspenseNotified(arc_self.id));
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
use super::{waker::ArcWake, Scheduler, SchedulerMsg};
|
||||
use futures_util::task::ArcWake;
|
||||
|
||||
use super::{Scheduler, SchedulerMsg};
|
||||
use crate::ScopeId;
|
||||
use std::cell::RefCell;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::task::Waker;
|
||||
|
||||
/// A task's unique identifier.
|
||||
///
|
||||
@@ -17,8 +20,7 @@ pub struct TaskId(pub usize);
|
||||
pub(crate) struct LocalTask {
|
||||
pub scope: ScopeId,
|
||||
pub(super) task: RefCell<Pin<Box<dyn Future<Output = ()> + 'static>>>,
|
||||
id: TaskId,
|
||||
tx: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
|
||||
pub waker: Waker,
|
||||
}
|
||||
|
||||
impl Scheduler {
|
||||
@@ -33,15 +35,20 @@ impl Scheduler {
|
||||
/// will only occur when the VirtuaalDom itself has been dropped.
|
||||
pub fn spawn(&self, scope: ScopeId, task: impl Future<Output = ()> + 'static) -> TaskId {
|
||||
let mut tasks = self.tasks.borrow_mut();
|
||||
|
||||
let entry = tasks.vacant_entry();
|
||||
let task_id = TaskId(entry.key());
|
||||
|
||||
entry.insert(Arc::new(LocalTask {
|
||||
id: task_id,
|
||||
tx: self.sender.clone(),
|
||||
let task = LocalTask {
|
||||
task: RefCell::new(Box::pin(task)),
|
||||
scope,
|
||||
}));
|
||||
waker: futures_util::task::waker(Arc::new(LocalTaskHandle {
|
||||
id: task_id,
|
||||
tx: self.sender.clone(),
|
||||
})),
|
||||
};
|
||||
|
||||
entry.insert(task);
|
||||
|
||||
self.sender
|
||||
.unbounded_send(SchedulerMsg::TaskNotified(task_id))
|
||||
@@ -58,10 +65,16 @@ impl Scheduler {
|
||||
}
|
||||
}
|
||||
|
||||
impl ArcWake for LocalTask {
|
||||
pub struct LocalTaskHandle {
|
||||
id: TaskId,
|
||||
tx: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
|
||||
}
|
||||
|
||||
impl ArcWake for LocalTaskHandle {
|
||||
fn wake_by_ref(arc_self: &Arc<Self>) {
|
||||
_ = arc_self
|
||||
arc_self
|
||||
.tx
|
||||
.unbounded_send(SchedulerMsg::TaskNotified(arc_self.id));
|
||||
.unbounded_send(SchedulerMsg::TaskNotified(arc_self.id))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ use crate::{
|
||||
ScopeId, TaskId, VNode, VirtualDom,
|
||||
};
|
||||
|
||||
use super::{waker::ArcWake, SuspenseId};
|
||||
use super::SuspenseId;
|
||||
|
||||
impl VirtualDom {
|
||||
/// Handle notifications by tasks inside the scheduler
|
||||
@@ -22,11 +22,11 @@ impl VirtualDom {
|
||||
|
||||
let task = match tasks.get(id.0) {
|
||||
Some(task) => task,
|
||||
// The task was removed from the scheduler, so we can just ignore it
|
||||
None => return,
|
||||
};
|
||||
|
||||
let waker = task.waker();
|
||||
let mut cx = Context::from_waker(&waker);
|
||||
let mut cx = Context::from_waker(&task.waker);
|
||||
|
||||
// If the task completes...
|
||||
if task.task.borrow_mut().as_mut().poll(&mut cx).is_ready() {
|
||||
@@ -45,19 +45,13 @@ impl VirtualDom {
|
||||
}
|
||||
|
||||
pub(crate) fn handle_suspense_wakeup(&mut self, id: SuspenseId) {
|
||||
let leaf = self
|
||||
.scheduler
|
||||
.leaves
|
||||
.borrow_mut()
|
||||
.get(id.0)
|
||||
.unwrap()
|
||||
.clone();
|
||||
let leaves = self.scheduler.leaves.borrow_mut();
|
||||
let leaf = leaves.get(id.0).unwrap();
|
||||
|
||||
let scope_id = leaf.scope_id;
|
||||
|
||||
// todo: cache the waker
|
||||
let waker = leaf.waker();
|
||||
let mut cx = Context::from_waker(&waker);
|
||||
let mut cx = Context::from_waker(&leaf.waker);
|
||||
|
||||
// Safety: the future is always pinned to the bump arena
|
||||
let mut pinned = unsafe { std::pin::Pin::new_unchecked(&mut *leaf.task) };
|
||||
@@ -91,6 +85,9 @@ impl VirtualDom {
|
||||
|
||||
let place_holder_id = scope.placeholder.get().unwrap();
|
||||
self.scope_stack.push(scope_id);
|
||||
|
||||
drop(leaves);
|
||||
|
||||
let created = self.create(template);
|
||||
self.scope_stack.pop();
|
||||
mutations.push(Mutation::ReplaceWith {
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
use std::mem;
|
||||
use std::sync::Arc;
|
||||
use std::task::{RawWaker, RawWakerVTable, Waker};
|
||||
|
||||
pub trait ArcWake: Sized {
|
||||
/// Create a waker from this self-wakening object
|
||||
fn waker(self: &Arc<Self>) -> Waker {
|
||||
unsafe fn rc_vtable<T: ArcWake>() -> &'static RawWakerVTable {
|
||||
&RawWakerVTable::new(
|
||||
|data| {
|
||||
let arc = mem::ManuallyDrop::new(Arc::<T>::from_raw(data.cast::<T>()));
|
||||
let _rc_clone: mem::ManuallyDrop<_> = arc.clone();
|
||||
RawWaker::new(data, rc_vtable::<T>())
|
||||
},
|
||||
|data| Arc::from_raw(data.cast::<T>()).wake(),
|
||||
|data| {
|
||||
let arc = mem::ManuallyDrop::new(Arc::<T>::from_raw(data.cast::<T>()));
|
||||
ArcWake::wake_by_ref(&arc);
|
||||
},
|
||||
|data| drop(Arc::<T>::from_raw(data.cast::<T>())),
|
||||
)
|
||||
}
|
||||
|
||||
unsafe {
|
||||
Waker::from_raw(RawWaker::new(
|
||||
Arc::into_raw(self.clone()).cast(),
|
||||
rc_vtable::<Self>(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn wake_by_ref(arc_self: &Arc<Self>);
|
||||
|
||||
fn wake(self: Arc<Self>) {
|
||||
Self::wake_by_ref(&self)
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,8 @@ use crate::{
|
||||
any_props::AnyProps,
|
||||
bump_frame::BumpFrame,
|
||||
innerlude::DirtyScope,
|
||||
innerlude::{SuspenseId, SuspenseLeaf},
|
||||
innerlude::{SuspenseHandle, SuspenseId, SuspenseLeaf},
|
||||
nodes::RenderReturn,
|
||||
scheduler::ArcWake,
|
||||
scopes::{ScopeId, ScopeState},
|
||||
virtual_dom::VirtualDom,
|
||||
};
|
||||
@@ -90,16 +89,17 @@ impl VirtualDom {
|
||||
let entry = leaves.vacant_entry();
|
||||
let suspense_id = SuspenseId(entry.key());
|
||||
|
||||
let leaf = Arc::new(SuspenseLeaf {
|
||||
let leaf = SuspenseLeaf {
|
||||
scope_id,
|
||||
task: task.as_mut(),
|
||||
id: suspense_id,
|
||||
tx: self.scheduler.sender.clone(),
|
||||
notified: Default::default(),
|
||||
});
|
||||
waker: futures_util::task::waker(Arc::new(SuspenseHandle {
|
||||
id: suspense_id,
|
||||
tx: self.scheduler.sender.clone(),
|
||||
})),
|
||||
};
|
||||
|
||||
let waker = leaf.waker();
|
||||
let mut cx = Context::from_waker(&waker);
|
||||
let mut cx = Context::from_waker(&leaf.waker);
|
||||
|
||||
// safety: the task is already pinned in the bump arena
|
||||
let mut pinned = unsafe { Pin::new_unchecked(task.as_mut()) };
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
# dioxus webview arch
|
||||
|
||||
|
||||
dioxus webview represents a usecase of dioxus to stream edits from one dom to another. In this particular case, we stream edits from a native process into the webview's runtime. WebView is really portable, so we can leverage the UI of iOS, Android, and all desktop platforms while also maintaining direct access to platform-specific APIs. It's the best of both worlds!
|
||||
|
||||
|
||||
For this to work properly, we need to ship a universal client that receives Dioxus edits from a remote process. To make our lives (as developers) a bit easier, the client code is currently implemented in JS (subject to change!).
|
||||
|
||||
The provider (the thing that talks to the client on the other side of the network) is still written in Rust, and is supplied through the dioxus-webview crate. The client is much more "dumb" than the provider - the provider must handle NodeID generation, flushing changes, priorities, managing events, and more.
|
||||
|
||||
|
||||
## update; wry!
|
||||
|
||||
Wry is an effort by the Tauri team to make fully native
|
||||
@@ -33,7 +33,7 @@ webbrowser = "0.8.0"
|
||||
infer = "0.11.0"
|
||||
dunce = "1.0.2"
|
||||
|
||||
interprocess = { version = "1.1.1", optional = true}
|
||||
interprocess = { version = "1.1.1", optional = true }
|
||||
futures-util = "0.3.25"
|
||||
|
||||
[target.'cfg(target_os = "ios")'.dependencies]
|
||||
|
||||
@@ -1,5 +1,57 @@
|
||||
# Dioxus-Desktop
|
||||
# Dioxus Desktop (webview)
|
||||
|
||||
Because the host VirtualDOM is running in its own native process, native applications can unlock their full potential. Dioxus-Desktop is designed to be a 100% rust alternative to ElectronJS without the memory overhead or bloat of ElectronJS apps.
|
||||
|
||||
By bridging the native process, desktop apps can access full multithreading power, peripheral support, hardware access, and native filesystem controls without the hassle of web technologies. Our goal with this desktop crate is to make it easy to ship both a web and native application, and quickly see large performance boosts without having to re-write the whole stack. As the dioxus ecosystem grows, we hope to see 3rd parties providing wrappers for storage, offline mode, etc that supports both web and native technologies.
|
||||
[![Crates.io][crates-badge]][crates-url]
|
||||
[![MIT licensed][mit-badge]][mit-url]
|
||||
[![Build Status][actions-badge]][actions-url]
|
||||
[![Discord chat][discord-badge]][discord-url]
|
||||
|
||||
[crates-badge]: https://img.shields.io/crates/v/dioxus-desktop.svg
|
||||
[crates-url]: https://crates.io/crates/dioxus-desktop
|
||||
|
||||
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||
[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
|
||||
|
||||
[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
|
||||
[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster
|
||||
|
||||
[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square
|
||||
[discord-url]: https://discord.gg/XgGxMSkvUM
|
||||
|
||||
[Website](https://dioxuslabs.com) |
|
||||
[Guides](https://dioxuslabs.com/guide/) |
|
||||
[API Docs](https://docs.rs/dioxus-desktop/latest/dioxus-desktop) |
|
||||
[Chat](https://discord.gg/XgGxMSkvUM)
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
`dioxus-desktop` provides a webview-based desktop renderer for the Dioxus VirtualDom.
|
||||
|
||||
This requires that webview is installed on the target system. WebView is installed by default on macOS and iOS devices, but might not come preinstalled on Windows or Linux devices. To fix these issues, follow the [instructions in the guide](guide-url).
|
||||
|
||||
[guide-url]: https://dioxuslabs.com/guide/setup.html#platform-specific-dependencies
|
||||
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
- Simple, one-line launch for desktop apps
|
||||
- Dioxus virtualdom running on a native thread
|
||||
- Full HTML/CSS support via `wry` and `tao`
|
||||
- Exposed `window` and `Proxy` types from tao for direct window manipulation
|
||||
- Helpful hooks for
|
||||
|
||||
## Contributing
|
||||
|
||||
- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
|
||||
- Join the discord and ask questions!
|
||||
|
||||
## License
|
||||
This project is licensed under the [MIT license].
|
||||
|
||||
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in Dioxus by you, shall be licensed as MIT, without any additional
|
||||
terms or conditions.
|
||||
@@ -1,154 +0,0 @@
|
||||
use crate::desktop_context::{DesktopContext, UserWindowEvent};
|
||||
use dioxus_core::*;
|
||||
use dioxus_html::HtmlEvent;
|
||||
use futures_channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender};
|
||||
use futures_util::StreamExt;
|
||||
#[cfg(target_os = "ios")]
|
||||
use objc::runtime::Object;
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::Arc,
|
||||
sync::{atomic::AtomicBool, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
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) eval_sender: tokio::sync::mpsc::UnboundedSender<Value>,
|
||||
pub(super) pending_edits: Arc<Mutex<Vec<String>>>,
|
||||
pub(super) quit_app_on_close: bool,
|
||||
pub(super) is_ready: Arc<AtomicBool>,
|
||||
pub(super) proxy: EventLoopProxy<UserWindowEvent>,
|
||||
|
||||
pub(super) event_tx: UnboundedSender<serde_json::Value>,
|
||||
#[cfg(debug_assertions)]
|
||||
pub(super) templates_tx: UnboundedSender<Template<'static>>,
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
pub(super) views: Vec<*mut Object>,
|
||||
}
|
||||
|
||||
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(Mutex::new(Vec::new()));
|
||||
let (event_tx, mut event_rx) = unbounded();
|
||||
let (templates_tx, mut templates_rx) = unbounded();
|
||||
let proxy2 = proxy.clone();
|
||||
|
||||
let pending_edits = edit_queue.clone();
|
||||
let desktop_context_proxy = proxy.clone();
|
||||
let (eval_sender, eval_reciever) = tokio::sync::mpsc::unbounded_channel::<Value>();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
// We create the runtime as multithreaded, so you can still "tokio::spawn" onto multiple threads
|
||||
// I'd personally not require tokio to be built-in to Dioxus-Desktop, but the DX is worse without it
|
||||
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
runtime.block_on(async move {
|
||||
let mut dom = VirtualDom::new_with_props(root, props)
|
||||
.with_root_context(DesktopContext::new(desktop_context_proxy, eval_reciever));
|
||||
{
|
||||
let edits = dom.rebuild();
|
||||
let mut queue = edit_queue.lock().unwrap();
|
||||
queue.push(serde_json::to_string(&edits).unwrap());
|
||||
proxy.send_event(UserWindowEvent::EditsReady).unwrap();
|
||||
}
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
template = {
|
||||
#[allow(unused)]
|
||||
fn maybe_future<'a>(templates_rx: &'a mut UnboundedReceiver<Template<'static>>) -> impl Future<Output = dioxus_core::Template<'static>> + 'a {
|
||||
#[cfg(debug_assertions)]
|
||||
return templates_rx.select_next_some();
|
||||
#[cfg(not(debug_assertions))]
|
||||
return std::future::pending();
|
||||
}
|
||||
maybe_future(&mut templates_rx)
|
||||
} => {
|
||||
dom.replace_template(template);
|
||||
}
|
||||
_ = dom.wait_for_work() => {}
|
||||
Some(json_value) = event_rx.next() => {
|
||||
if let Ok(value) = serde_json::from_value::<HtmlEvent>(json_value) {
|
||||
let HtmlEvent {
|
||||
name,
|
||||
element,
|
||||
bubbles,
|
||||
data
|
||||
} = value;
|
||||
dom.handle_event(&name, data.into_any(), element, bubbles);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let muts = dom
|
||||
.render_with_deadline(tokio::time::sleep(Duration::from_millis(16)))
|
||||
.await;
|
||||
|
||||
edit_queue
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(serde_json::to_string(&muts).unwrap());
|
||||
let _ = proxy.send_event(UserWindowEvent::EditsReady);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
Self {
|
||||
pending_edits,
|
||||
eval_sender,
|
||||
webviews: HashMap::new(),
|
||||
is_ready: Arc::new(AtomicBool::new(false)),
|
||||
quit_app_on_close: true,
|
||||
proxy: proxy2,
|
||||
event_tx,
|
||||
#[cfg(debug_assertions)]
|
||||
templates_tx,
|
||||
#[cfg(target_os = "ios")]
|
||||
views: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
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 new_queue = Vec::new();
|
||||
|
||||
{
|
||||
let mut queue = self.pending_edits.lock().unwrap();
|
||||
std::mem::swap(&mut new_queue, &mut *queue);
|
||||
}
|
||||
|
||||
let (_id, view) = self.webviews.iter_mut().next().unwrap();
|
||||
|
||||
for edit in new_queue.drain(..) {
|
||||
view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,15 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::controller::DesktopController;
|
||||
use crate::eval::EvalResult;
|
||||
use crate::events::IpcMessage;
|
||||
use dioxus_core::ScopeState;
|
||||
use serde::de::Error;
|
||||
use serde_json::Value;
|
||||
use std::future::Future;
|
||||
use std::future::IntoFuture;
|
||||
use std::pin::Pin;
|
||||
use wry::application::dpi::LogicalSize;
|
||||
use wry::application::event_loop::ControlFlow;
|
||||
use wry::application::event_loop::EventLoopProxy;
|
||||
#[cfg(target_os = "ios")]
|
||||
use wry::application::platform::ios::WindowExtIOS;
|
||||
use wry::application::window::Fullscreen as WryFullscreen;
|
||||
|
||||
use UserWindowEvent::*;
|
||||
use wry::application::window::Window;
|
||||
use wry::webview::WebView;
|
||||
|
||||
pub type ProxyType = EventLoopProxy<UserWindowEvent>;
|
||||
|
||||
@@ -40,18 +35,35 @@ pub fn use_window(cx: &ScopeState) -> &DesktopContext {
|
||||
#[derive(Clone)]
|
||||
pub struct DesktopContext {
|
||||
/// The wry/tao proxy to the current window
|
||||
pub webview: Rc<WebView>,
|
||||
|
||||
/// The proxy to the event loop
|
||||
pub proxy: ProxyType,
|
||||
pub(super) eval_reciever: Rc<tokio::sync::Mutex<tokio::sync::mpsc::UnboundedReceiver<Value>>>,
|
||||
|
||||
/// The receiver for eval results since eval is async
|
||||
pub(super) eval: tokio::sync::broadcast::Sender<Value>,
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
pub(crate) views: Rc<RefCell<Vec<*mut objc::runtime::Object>>>,
|
||||
}
|
||||
|
||||
/// A smart pointer to the current window.
|
||||
impl std::ops::Deref for DesktopContext {
|
||||
type Target = Window;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.webview.window()
|
||||
}
|
||||
}
|
||||
|
||||
impl DesktopContext {
|
||||
pub(crate) fn new(
|
||||
proxy: ProxyType,
|
||||
eval_reciever: tokio::sync::mpsc::UnboundedReceiver<Value>,
|
||||
) -> Self {
|
||||
pub(crate) fn new(webview: Rc<WebView>, proxy: ProxyType) -> Self {
|
||||
Self {
|
||||
webview,
|
||||
proxy,
|
||||
eval_reciever: Rc::new(tokio::sync::Mutex::new(eval_reciever)),
|
||||
eval: tokio::sync::broadcast::channel(8).0,
|
||||
#[cfg(target_os = "ios")]
|
||||
views: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,283 +76,125 @@ impl DesktopContext {
|
||||
/// onmousedown: move |_| { desktop.drag_window(); }
|
||||
/// ```
|
||||
pub fn drag(&self) {
|
||||
let _ = self.proxy.send_event(DragWindow);
|
||||
let window = self.webview.window();
|
||||
|
||||
// if the drag_window has any errors, we don't do anything
|
||||
window.fullscreen().is_none().then(|| window.drag_window());
|
||||
}
|
||||
|
||||
/// set window minimize state
|
||||
pub fn set_minimized(&self, minimized: bool) {
|
||||
let _ = self.proxy.send_event(Minimize(minimized));
|
||||
}
|
||||
|
||||
/// set window maximize state
|
||||
pub fn set_maximized(&self, maximized: bool) {
|
||||
let _ = self.proxy.send_event(Maximize(maximized));
|
||||
}
|
||||
|
||||
/// toggle window maximize state
|
||||
/// Toggle whether the window is maximized or not
|
||||
pub fn toggle_maximized(&self) {
|
||||
let _ = self.proxy.send_event(MaximizeToggle);
|
||||
}
|
||||
let window = self.webview.window();
|
||||
|
||||
/// set window visible or not
|
||||
pub fn set_visible(&self, visible: bool) {
|
||||
let _ = self.proxy.send_event(Visible(visible));
|
||||
window.set_maximized(!window.is_maximized())
|
||||
}
|
||||
|
||||
/// close window
|
||||
pub fn close(&self) {
|
||||
let _ = self.proxy.send_event(CloseWindow);
|
||||
}
|
||||
|
||||
/// set window to focus
|
||||
pub fn focus(&self) {
|
||||
let _ = self.proxy.send_event(FocusWindow);
|
||||
let _ = self.proxy.send_event(UserWindowEvent::CloseWindow);
|
||||
}
|
||||
|
||||
/// change window to fullscreen
|
||||
pub fn set_fullscreen(&self, fullscreen: bool) {
|
||||
let _ = self.proxy.send_event(Fullscreen(fullscreen));
|
||||
}
|
||||
|
||||
/// set resizable state
|
||||
pub fn set_resizable(&self, resizable: bool) {
|
||||
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(AlwaysOnTop(top));
|
||||
}
|
||||
|
||||
/// set cursor visible or not
|
||||
pub fn set_cursor_visible(&self, visible: bool) {
|
||||
let _ = self.proxy.send_event(CursorVisible(visible));
|
||||
}
|
||||
|
||||
/// set cursor grab
|
||||
pub fn set_cursor_grab(&self, grab: bool) {
|
||||
let _ = self.proxy.send_event(CursorGrab(grab));
|
||||
}
|
||||
|
||||
/// set window title
|
||||
pub fn set_title(&self, title: &str) {
|
||||
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(SetDecorations(decoration));
|
||||
}
|
||||
|
||||
/// set window zoom level
|
||||
pub fn set_zoom_level(&self, scale_factor: f64) {
|
||||
let _ = self.proxy.send_event(SetZoomLevel(scale_factor));
|
||||
}
|
||||
|
||||
/// modifies the inner size of the window
|
||||
pub fn set_inner_size(&self, logical_size: LogicalSize<f64>) {
|
||||
let _ = self.proxy.send_event(SetInnerSize(logical_size));
|
||||
if let Some(handle) = self.webview.window().current_monitor() {
|
||||
self.webview
|
||||
.window()
|
||||
.set_fullscreen(fullscreen.then_some(WryFullscreen::Borderless(Some(handle))));
|
||||
}
|
||||
}
|
||||
|
||||
/// launch print modal
|
||||
pub fn print(&self) {
|
||||
let _ = self.proxy.send_event(Print);
|
||||
if let Err(e) = self.webview.print() {
|
||||
log::warn!("Open print modal failed: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the zoom level of the webview
|
||||
pub fn set_zoom_level(&self, level: f64) {
|
||||
self.webview.zoom(level);
|
||||
}
|
||||
|
||||
/// opens DevTool window
|
||||
pub fn devtool(&self) {
|
||||
let _ = self.proxy.send_event(DevTool);
|
||||
#[cfg(debug_assertions)]
|
||||
self.webview.open_devtools();
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
log::warn!("Devtools are disabled in release builds");
|
||||
}
|
||||
|
||||
/// run (evaluate) a script in the WebView context
|
||||
pub fn eval(&self, script: impl std::string::ToString) {
|
||||
let _ = self.proxy.send_event(Eval(script.to_string()));
|
||||
/// Evaluate a javascript expression
|
||||
pub fn eval(&self, code: &str) -> EvalResult {
|
||||
// Embed the return of the eval in a function so we can send it back to the main thread
|
||||
let script = format!(
|
||||
r#"
|
||||
window.ipc.postMessage(
|
||||
JSON.stringify({{
|
||||
"method":"eval_result",
|
||||
"params": (
|
||||
function(){{
|
||||
{}
|
||||
}}
|
||||
)()
|
||||
}})
|
||||
);
|
||||
"#,
|
||||
code
|
||||
);
|
||||
|
||||
if let Err(e) = self.webview.evaluate_script(&script) {
|
||||
// send an error to the eval receiver
|
||||
log::warn!("Eval script error: {e}");
|
||||
}
|
||||
|
||||
EvalResult::new(self.eval.clone())
|
||||
}
|
||||
|
||||
/// Push view
|
||||
/// Push an objc view to the window
|
||||
#[cfg(target_os = "ios")]
|
||||
pub fn push_view(&self, view: objc_id::ShareId<objc::runtime::Object>) {
|
||||
let _ = self.proxy.send_event(PushView(view));
|
||||
let window = self.webview.window();
|
||||
|
||||
unsafe {
|
||||
use objc::runtime::Object;
|
||||
use objc::*;
|
||||
assert!(is_main_thread());
|
||||
let ui_view = window.ui_view() as *mut Object;
|
||||
let ui_view_frame: *mut Object = msg_send![ui_view, frame];
|
||||
let _: () = msg_send![view, setFrame: ui_view_frame];
|
||||
let _: () = msg_send![view, setAutoresizingMask: 31];
|
||||
|
||||
let ui_view_controller = window.ui_view_controller() as *mut Object;
|
||||
let _: () = msg_send![ui_view_controller, setView: view];
|
||||
self.views.borrow_mut().push(ui_view);
|
||||
}
|
||||
}
|
||||
|
||||
/// Push view
|
||||
/// Pop an objc view from the window
|
||||
#[cfg(target_os = "ios")]
|
||||
pub fn pop_view(&self) {
|
||||
let _ = self.proxy.send_event(PopView);
|
||||
let window = self.webview.window();
|
||||
|
||||
unsafe {
|
||||
use objc::runtime::Object;
|
||||
use objc::*;
|
||||
assert!(is_main_thread());
|
||||
if let Some(view) = self.views.borrow_mut().pop() {
|
||||
let ui_view_controller = window.ui_view_controller() as *mut Object;
|
||||
let _: () = msg_send![ui_view_controller, setView: view];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum UserWindowEvent {
|
||||
EditsReady,
|
||||
Initialize,
|
||||
Poll,
|
||||
|
||||
Ipc(IpcMessage),
|
||||
|
||||
CloseWindow,
|
||||
DragWindow,
|
||||
FocusWindow,
|
||||
|
||||
Visible(bool),
|
||||
Minimize(bool),
|
||||
Maximize(bool),
|
||||
MaximizeToggle,
|
||||
Resizable(bool),
|
||||
AlwaysOnTop(bool),
|
||||
Fullscreen(bool),
|
||||
|
||||
CursorVisible(bool),
|
||||
CursorGrab(bool),
|
||||
|
||||
SetTitle(String),
|
||||
SetDecorations(bool),
|
||||
|
||||
SetZoomLevel(f64),
|
||||
SetInnerSize(LogicalSize<f64>),
|
||||
|
||||
Print,
|
||||
DevTool,
|
||||
|
||||
Eval(String),
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
PushView(objc_id::ShareId<objc::runtime::Object>),
|
||||
#[cfg(target_os = "ios")]
|
||||
PopView,
|
||||
}
|
||||
|
||||
impl DesktopController {
|
||||
pub(super) fn handle_event(
|
||||
&mut self,
|
||||
user_event: UserWindowEvent,
|
||||
control_flow: &mut ControlFlow,
|
||||
) {
|
||||
// currently dioxus-desktop supports a single window only,
|
||||
// so we can grab the only webview from the map;
|
||||
// on wayland it is possible that a user event is emitted
|
||||
// before the webview is initialized. ignore the event.
|
||||
let webview = if let Some(webview) = self.webviews.values().next() {
|
||||
webview
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
let window = webview.window();
|
||||
|
||||
match user_event {
|
||||
Initialize | EditsReady => self.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_some(WryFullscreen::Borderless(Some(handle))));
|
||||
}
|
||||
}
|
||||
FocusWindow => window.set_focus(),
|
||||
Resizable(state) => window.set_resizable(state),
|
||||
AlwaysOnTop(state) => window.set_always_on_top(state),
|
||||
|
||||
Eval(code) => {
|
||||
let script = format!(
|
||||
r#"window.ipc.postMessage(JSON.stringify({{"method":"eval_result", params: (function(){{
|
||||
{}
|
||||
}})()}}));"#,
|
||||
code
|
||||
);
|
||||
if let Err(e) = webview.evaluate_script(&script) {
|
||||
// we can't panic this error.
|
||||
log::warn!("Eval script error: {e}");
|
||||
}
|
||||
}
|
||||
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),
|
||||
|
||||
SetZoomLevel(scale_factor) => webview.zoom(scale_factor),
|
||||
SetInnerSize(logical_size) => window.set_inner_size(logical_size),
|
||||
|
||||
Print => {
|
||||
if let Err(e) = webview.print() {
|
||||
// we can't panic this error.
|
||||
log::warn!("Open print modal failed: {e}");
|
||||
}
|
||||
}
|
||||
DevTool => {
|
||||
#[cfg(debug_assertions)]
|
||||
webview.open_devtools();
|
||||
#[cfg(not(debug_assertions))]
|
||||
log::warn!("Devtools are disabled in release builds");
|
||||
}
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
PushView(view) => unsafe {
|
||||
use objc::runtime::Object;
|
||||
use objc::*;
|
||||
assert!(is_main_thread());
|
||||
let ui_view = window.ui_view() as *mut Object;
|
||||
let ui_view_frame: *mut Object = msg_send![ui_view, frame];
|
||||
let _: () = msg_send![view, setFrame: ui_view_frame];
|
||||
let _: () = msg_send![view, setAutoresizingMask: 31];
|
||||
|
||||
let ui_view_controller = window.ui_view_controller() as *mut Object;
|
||||
let _: () = msg_send![ui_view_controller, setView: view];
|
||||
self.views.push(ui_view);
|
||||
},
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
PopView => unsafe {
|
||||
use objc::runtime::Object;
|
||||
use objc::*;
|
||||
assert!(is_main_thread());
|
||||
if let Some(view) = self.views.pop() {
|
||||
let ui_view_controller = window.ui_view_controller() as *mut Object;
|
||||
let _: () = msg_send![ui_view_controller, setView: view];
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a closure that executes any JavaScript in the WebView context.
|
||||
pub fn use_eval<S: std::string::ToString>(cx: &ScopeState) -> &dyn Fn(S) -> EvalResult {
|
||||
let desktop = use_window(cx).clone();
|
||||
cx.use_hook(|| {
|
||||
move |script| {
|
||||
desktop.eval(script);
|
||||
let recv = desktop.eval_reciever.clone();
|
||||
EvalResult { reciever: recv }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// A future that resolves to the result of a JavaScript evaluation.
|
||||
pub struct EvalResult {
|
||||
reciever: Rc<tokio::sync::Mutex<tokio::sync::mpsc::UnboundedReceiver<serde_json::Value>>>,
|
||||
}
|
||||
|
||||
impl IntoFuture for EvalResult {
|
||||
type Output = Result<serde_json::Value, serde_json::Error>;
|
||||
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Result<serde_json::Value, serde_json::Error>>>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(async move {
|
||||
let mut reciever = self.reciever.lock().await;
|
||||
match reciever.recv().await {
|
||||
Some(result) => Ok(result),
|
||||
None => Err(serde_json::Error::custom("No result returned")),
|
||||
}
|
||||
}) as Pin<Box<dyn Future<Output = Result<serde_json::Value, serde_json::Error>>>>
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::use_window;
|
||||
use dioxus_core::ScopeState;
|
||||
use serde::de::Error;
|
||||
use std::future::Future;
|
||||
use std::future::IntoFuture;
|
||||
use std::pin::Pin;
|
||||
|
||||
/// A future that resolves to the result of a JavaScript evaluation.
|
||||
pub struct EvalResult {
|
||||
pub(crate) broadcast: tokio::sync::broadcast::Sender<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl EvalResult {
|
||||
pub(crate) fn new(sender: tokio::sync::broadcast::Sender<serde_json::Value>) -> Self {
|
||||
Self { broadcast: sender }
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoFuture for EvalResult {
|
||||
type Output = Result<serde_json::Value, serde_json::Error>;
|
||||
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Result<serde_json::Value, serde_json::Error>>>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(async move {
|
||||
let mut reciever = self.broadcast.subscribe();
|
||||
match reciever.recv().await {
|
||||
Ok(result) => Ok(result),
|
||||
Err(_) => Err(serde_json::Error::custom("No result returned")),
|
||||
}
|
||||
}) as Pin<Box<dyn Future<Output = Result<serde_json::Value, serde_json::Error>>>>
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a closure that executes any JavaScript in the WebView context.
|
||||
pub fn use_eval(cx: &ScopeState) -> &Rc<dyn Fn(String) -> EvalResult> {
|
||||
let desktop = use_window(cx);
|
||||
&*cx.use_hook(|| {
|
||||
let desktop = desktop.clone();
|
||||
|
||||
Rc::new(move |script: String| desktop.eval(&script)) as Rc<dyn Fn(String) -> EvalResult>
|
||||
})
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub(crate) struct IpcMessage {
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct IpcMessage {
|
||||
method: String,
|
||||
params: serde_json::Value,
|
||||
}
|
||||
@@ -17,13 +17,3 @@ impl IpcMessage {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
+124
-150
@@ -4,36 +4,35 @@
|
||||
#![deny(missing_docs)]
|
||||
|
||||
mod cfg;
|
||||
mod controller;
|
||||
mod desktop_context;
|
||||
mod escape;
|
||||
mod eval;
|
||||
mod events;
|
||||
mod protocol;
|
||||
mod waker;
|
||||
mod webview;
|
||||
|
||||
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
||||
mod hot_reload;
|
||||
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
|
||||
use desktop_context::UserWindowEvent;
|
||||
pub use desktop_context::{use_eval, use_window, DesktopContext, EvalResult};
|
||||
use futures_channel::mpsc::UnboundedSender;
|
||||
pub use wry;
|
||||
pub use wry::application as tao;
|
||||
|
||||
pub use cfg::Config;
|
||||
use controller::DesktopController;
|
||||
use desktop_context::UserWindowEvent;
|
||||
pub use desktop_context::{use_window, DesktopContext};
|
||||
use dioxus_core::*;
|
||||
use events::parse_ipc_message;
|
||||
use dioxus_html::HtmlEvent;
|
||||
pub use eval::{use_eval, EvalResult};
|
||||
use futures_util::{pin_mut, FutureExt};
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
use std::task::Waker;
|
||||
pub use tao::dpi::{LogicalSize, PhysicalSize};
|
||||
pub use tao::window::WindowBuilder;
|
||||
use tao::{
|
||||
event::{Event, StartCause, WindowEvent},
|
||||
event_loop::{ControlFlow, EventLoop},
|
||||
window::Window,
|
||||
};
|
||||
use wry::webview::WebViewBuilder;
|
||||
pub use wry;
|
||||
pub use wry::application as tao;
|
||||
|
||||
/// Launch the WebView and run the event loop.
|
||||
///
|
||||
@@ -81,7 +80,7 @@ pub fn launch_cfg(root: Component, config_builder: Config) {
|
||||
|
||||
/// Launch the WebView and run the event loop, with configuration and root props.
|
||||
///
|
||||
/// This function will start a multithreaded Tokio runtime as well the WebView event loop.
|
||||
/// This function will start a multithreaded Tokio runtime as well the WebView event loop. This will block the current thread.
|
||||
///
|
||||
/// You can configure the WebView window with a configuration closure
|
||||
///
|
||||
@@ -89,7 +88,7 @@ pub fn launch_cfg(root: Component, config_builder: Config) {
|
||||
/// use dioxus::prelude::*;
|
||||
///
|
||||
/// fn main() {
|
||||
/// dioxus_desktop::launch_with_props(app, AppProps { name: "asd" }, |c| c);
|
||||
/// dioxus_desktop::launch_with_props(app, AppProps { name: "asd" }, Config::default());
|
||||
/// }
|
||||
///
|
||||
/// struct AppProps {
|
||||
@@ -102,165 +101,140 @@ pub fn launch_cfg(root: Component, config_builder: Config) {
|
||||
/// })
|
||||
/// }
|
||||
/// ```
|
||||
pub fn launch_with_props<P: 'static + Send>(root: Component<P>, props: P, mut cfg: Config) {
|
||||
let event_loop = EventLoop::with_user_event();
|
||||
let mut desktop = DesktopController::new_on_tokio(root, props, event_loop.create_proxy());
|
||||
pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, mut cfg: Config) {
|
||||
let mut dom = VirtualDom::new_with_props(root, props);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
hot_reload::init(desktop.templates_tx.clone());
|
||||
let event_loop = EventLoop::with_user_event();
|
||||
|
||||
let proxy = event_loop.create_proxy();
|
||||
|
||||
// We start the tokio runtime *on this thread*
|
||||
// Any future we poll later will use this runtime to spawn tasks and for IO
|
||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// We enter the runtime but we poll futures manually, circumventing the per-task runtime budget
|
||||
let _guard = rt.enter();
|
||||
|
||||
// We want to poll the virtualdom and the event loop at the same time, so the waker will be connected to both
|
||||
let waker = waker::tao_waker(&proxy);
|
||||
|
||||
// We only have one webview right now, but we'll have more later
|
||||
// Store them in a hashmap so we can remove them when they're closed
|
||||
let mut webviews = HashMap::new();
|
||||
|
||||
event_loop.run(move |window_event, event_loop, control_flow| {
|
||||
*control_flow = ControlFlow::Wait;
|
||||
|
||||
match window_event {
|
||||
Event::NewEvents(StartCause::Init) => desktop.start(&mut cfg, event_loop),
|
||||
Event::UserEvent(UserWindowEvent::CloseWindow) => *control_flow = ControlFlow::Exit,
|
||||
|
||||
Event::WindowEvent {
|
||||
event, window_id, ..
|
||||
} => match event {
|
||||
WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
|
||||
WindowEvent::Destroyed { .. } => desktop.close_window(window_id, control_flow),
|
||||
WindowEvent::Destroyed { .. } => {
|
||||
webviews.remove(&window_id);
|
||||
|
||||
if webviews.is_empty() {
|
||||
*control_flow = ControlFlow::Exit;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
|
||||
Event::UserEvent(user_event) => desktop.handle_event(user_event, control_flow),
|
||||
Event::MainEventsCleared => {}
|
||||
Event::Resumed => {}
|
||||
Event::Suspended => {}
|
||||
Event::LoopDestroyed => {}
|
||||
Event::RedrawRequested(_id) => {}
|
||||
Event::NewEvents(StartCause::Init) => {
|
||||
let window = webview::build(&mut cfg, event_loop, proxy.clone());
|
||||
|
||||
dom.base_scope()
|
||||
.provide_context(DesktopContext::new(window.clone(), proxy.clone()));
|
||||
|
||||
webviews.insert(window.window().id(), window);
|
||||
|
||||
_ = proxy.send_event(UserWindowEvent::Poll);
|
||||
}
|
||||
|
||||
Event::UserEvent(UserWindowEvent::Poll) => {
|
||||
poll_vdom(&waker, &mut dom, &mut webviews);
|
||||
}
|
||||
|
||||
Event::UserEvent(UserWindowEvent::Ipc(msg)) if msg.method() == "user_event" => {
|
||||
let evt = match serde_json::from_value::<HtmlEvent>(msg.params()) {
|
||||
Ok(value) => value,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
dom.handle_event(&evt.name, evt.data.into_any(), evt.element, evt.bubbles);
|
||||
|
||||
send_edits(dom.render_immediate(), &mut webviews);
|
||||
}
|
||||
|
||||
Event::UserEvent(UserWindowEvent::Ipc(msg)) if msg.method() == "initialize" => {
|
||||
send_edits(dom.rebuild(), &mut webviews);
|
||||
}
|
||||
|
||||
// When the webview chirps back with the result of the eval, we send it to the active receiver
|
||||
//
|
||||
// This currently doesn't perform any targeting to the callsite, so if you eval multiple times at once,
|
||||
// you might the wrong result. This should be fixed
|
||||
Event::UserEvent(UserWindowEvent::Ipc(msg)) if msg.method() == "eval_result" => {
|
||||
dom.base_scope()
|
||||
.consume_context::<DesktopContext>()
|
||||
.unwrap()
|
||||
.eval
|
||||
.send(msg.params())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
Event::UserEvent(UserWindowEvent::Ipc(msg)) if msg.method() == "browser_open" => {
|
||||
if let Some(temp) = msg.params().as_object() {
|
||||
if temp.contains_key("href") {
|
||||
let open = webbrowser::open(temp["href"].as_str().unwrap());
|
||||
if let Err(e) = open {
|
||||
log::error!("Open Browser error: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
impl DesktopController {
|
||||
fn start(
|
||||
&mut self,
|
||||
cfg: &mut Config,
|
||||
event_loop: &tao::event_loop::EventLoopWindowTarget<UserWindowEvent>,
|
||||
) {
|
||||
let webview = build_webview(
|
||||
cfg,
|
||||
event_loop,
|
||||
self.is_ready.clone(),
|
||||
self.proxy.clone(),
|
||||
self.eval_sender.clone(),
|
||||
self.event_tx.clone(),
|
||||
);
|
||||
type Webviews = HashMap<tao::window::WindowId, Rc<wry::webview::WebView>>;
|
||||
|
||||
self.webviews.insert(webview.window().id(), webview);
|
||||
}
|
||||
}
|
||||
/// Poll the virtualdom until it's pending
|
||||
///
|
||||
/// The waker we give it is connected to the event loop, so it will wake up the event loop when it's ready to be polled again
|
||||
///
|
||||
/// All IO is done on the tokio runtime we started earlier
|
||||
fn poll_vdom(waker: &Waker, dom: &mut VirtualDom, webviews: &mut Webviews) {
|
||||
let mut cx = std::task::Context::from_waker(waker);
|
||||
|
||||
fn build_webview(
|
||||
cfg: &mut Config,
|
||||
event_loop: &tao::event_loop::EventLoopWindowTarget<UserWindowEvent>,
|
||||
is_ready: Arc<AtomicBool>,
|
||||
proxy: tao::event_loop::EventLoopProxy<UserWindowEvent>,
|
||||
eval_sender: tokio::sync::mpsc::UnboundedSender<serde_json::Value>,
|
||||
event_tx: UnboundedSender<serde_json::Value>,
|
||||
) -> wry::webview::WebView {
|
||||
let builder = cfg.window.clone();
|
||||
let window = builder.build(event_loop).unwrap();
|
||||
let file_handler = cfg.file_drop_handler.take();
|
||||
let custom_head = cfg.custom_head.clone();
|
||||
let resource_dir = cfg.resource_dir.clone();
|
||||
let index_file = cfg.custom_index.clone();
|
||||
let root_name = cfg.root_name.clone();
|
||||
loop {
|
||||
{
|
||||
let fut = dom.wait_for_work();
|
||||
pin_mut!(fut);
|
||||
|
||||
// We assume that if the icon is None in cfg, then the user just didnt set it
|
||||
if cfg.window.window.window_icon.is_none() {
|
||||
window.set_window_icon(Some(
|
||||
tao::window::Icon::from_rgba(
|
||||
include_bytes!("./assets/default_icon.bin").to_vec(),
|
||||
460,
|
||||
460,
|
||||
)
|
||||
.expect("image parse failed"),
|
||||
));
|
||||
}
|
||||
|
||||
let mut webview = WebViewBuilder::new(window)
|
||||
.unwrap()
|
||||
.with_transparent(cfg.window.window.transparent)
|
||||
.with_url("dioxus://index.html/")
|
||||
.unwrap()
|
||||
.with_ipc_handler(move |_window: &Window, payload: String| {
|
||||
let message = match parse_ipc_message(&payload) {
|
||||
Some(message) => message,
|
||||
None => {
|
||||
log::error!("Failed to parse IPC message: {}", payload);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match message.method() {
|
||||
"eval_result" => {
|
||||
let result = message.params();
|
||||
eval_sender.send(result).unwrap();
|
||||
}
|
||||
"user_event" => {
|
||||
_ = event_tx.unbounded_send(message.params());
|
||||
}
|
||||
"initialize" => {
|
||||
is_ready.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
let _ = proxy.send_event(UserWindowEvent::EditsReady);
|
||||
}
|
||||
"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) {
|
||||
log::error!("Open Browser error: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
match fut.poll_unpin(&mut cx) {
|
||||
std::task::Poll::Ready(_) => {}
|
||||
std::task::Poll::Pending => break,
|
||||
}
|
||||
})
|
||||
.with_custom_protocol(String::from("dioxus"), move |r| {
|
||||
protocol::desktop_handler(
|
||||
r,
|
||||
resource_dir.clone(),
|
||||
custom_head.clone(),
|
||||
index_file.clone(),
|
||||
&root_name,
|
||||
)
|
||||
})
|
||||
.with_file_drop_handler(move |window, evet| {
|
||||
file_handler
|
||||
.as_ref()
|
||||
.map(|handler| handler(window, evet))
|
||||
.unwrap_or_default()
|
||||
});
|
||||
}
|
||||
|
||||
for (name, handler) in cfg.protocols.drain(..) {
|
||||
webview = webview.with_custom_protocol(name, handler)
|
||||
send_edits(dom.render_immediate(), webviews);
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.disable_context_menu {
|
||||
// in release mode, we don't want to show the dev tool or reload menus
|
||||
webview = webview.with_initialization_script(
|
||||
r#"
|
||||
if (document.addEventListener) {
|
||||
document.addEventListener('contextmenu', function(e) {
|
||||
e.preventDefault();
|
||||
}, false);
|
||||
} else {
|
||||
document.attachEvent('oncontextmenu', function() {
|
||||
window.event.returnValue = false;
|
||||
});
|
||||
}
|
||||
"#,
|
||||
)
|
||||
} else {
|
||||
// in debug, we are okay with the reload menu showing and dev tool
|
||||
webview = webview.with_devtools(true);
|
||||
}
|
||||
/// Send a list of mutations to the webview
|
||||
fn send_edits(edits: Mutations, webviews: &mut Webviews) {
|
||||
let serialized = serde_json::to_string(&edits).unwrap();
|
||||
|
||||
webview.build().unwrap()
|
||||
let (_id, view) = webviews.iter_mut().next().unwrap();
|
||||
|
||||
// todo: use SSE and binary data to send the edits with lower overhead
|
||||
_ = view.evaluate_script(&format!("window.interpreter.handleEdits({})", serialized));
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
use crate::desktop_context::UserWindowEvent;
|
||||
use futures_util::task::ArcWake;
|
||||
use std::sync::Arc;
|
||||
use wry::application::event_loop::EventLoopProxy;
|
||||
|
||||
/// Create a waker that will send a poll event to the event loop.
|
||||
///
|
||||
/// This lets the VirtualDom "come up for air" and process events while the main thread is blocked by the WebView.
|
||||
///
|
||||
/// All other IO lives in the Tokio runtime,
|
||||
pub fn tao_waker(proxy: &EventLoopProxy<UserWindowEvent>) -> std::task::Waker {
|
||||
struct DomHandle(EventLoopProxy<UserWindowEvent>);
|
||||
|
||||
// this should be implemented by most platforms, but ios is missing this until
|
||||
// https://github.com/tauri-apps/wry/issues/830 is resolved
|
||||
unsafe impl Send for DomHandle {}
|
||||
unsafe impl Sync for DomHandle {}
|
||||
|
||||
impl ArcWake for DomHandle {
|
||||
fn wake_by_ref(arc_self: &Arc<Self>) {
|
||||
_ = arc_self.0.send_event(UserWindowEvent::Poll);
|
||||
}
|
||||
}
|
||||
|
||||
futures_util::task::waker(Arc::new(DomHandle(proxy.clone())))
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::protocol;
|
||||
use crate::{desktop_context::UserWindowEvent, Config};
|
||||
use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget};
|
||||
pub use wry;
|
||||
pub use wry::application as tao;
|
||||
use wry::application::window::Window;
|
||||
use wry::webview::{WebView, WebViewBuilder};
|
||||
|
||||
pub fn build(
|
||||
cfg: &mut Config,
|
||||
event_loop: &EventLoopWindowTarget<UserWindowEvent>,
|
||||
proxy: EventLoopProxy<UserWindowEvent>,
|
||||
) -> Rc<WebView> {
|
||||
let builder = cfg.window.clone();
|
||||
let window = builder.build(event_loop).unwrap();
|
||||
let file_handler = cfg.file_drop_handler.take();
|
||||
let custom_head = cfg.custom_head.clone();
|
||||
let resource_dir = cfg.resource_dir.clone();
|
||||
let index_file = cfg.custom_index.clone();
|
||||
let root_name = cfg.root_name.clone();
|
||||
|
||||
// We assume that if the icon is None in cfg, then the user just didnt set it
|
||||
if cfg.window.window.window_icon.is_none() {
|
||||
window.set_window_icon(Some(
|
||||
tao::window::Icon::from_rgba(
|
||||
include_bytes!("./assets/default_icon.bin").to_vec(),
|
||||
460,
|
||||
460,
|
||||
)
|
||||
.expect("image parse failed"),
|
||||
));
|
||||
}
|
||||
|
||||
let mut webview = WebViewBuilder::new(window)
|
||||
.unwrap()
|
||||
.with_transparent(cfg.window.window.transparent)
|
||||
.with_url("dioxus://index.html/")
|
||||
.unwrap()
|
||||
.with_ipc_handler(move |_window: &Window, payload: String| {
|
||||
// defer the event to the main thread
|
||||
if let Ok(message) = serde_json::from_str(&payload) {
|
||||
_ = proxy.send_event(UserWindowEvent::Ipc(message));
|
||||
}
|
||||
})
|
||||
.with_custom_protocol(String::from("dioxus"), move |r| {
|
||||
protocol::desktop_handler(
|
||||
r,
|
||||
resource_dir.clone(),
|
||||
custom_head.clone(),
|
||||
index_file.clone(),
|
||||
&root_name,
|
||||
)
|
||||
})
|
||||
.with_file_drop_handler(move |window, evet| {
|
||||
file_handler
|
||||
.as_ref()
|
||||
.map(|handler| handler(window, evet))
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
||||
for (name, handler) in cfg.protocols.drain(..) {
|
||||
webview = webview.with_custom_protocol(name, handler)
|
||||
}
|
||||
|
||||
if cfg.disable_context_menu {
|
||||
// in release mode, we don't want to show the dev tool or reload menus
|
||||
webview = webview.with_initialization_script(
|
||||
r#"
|
||||
if (document.addEventListener) {
|
||||
document.addEventListener('contextmenu', function(e) {
|
||||
e.preventDefault();
|
||||
}, false);
|
||||
} else {
|
||||
document.attachEvent('oncontextmenu', function() {
|
||||
window.event.returnValue = false;
|
||||
});
|
||||
}
|
||||
"#,
|
||||
)
|
||||
} else {
|
||||
// in debug, we are okay with the reload menu showing and dev tool
|
||||
webview = webview.with_devtools(true);
|
||||
}
|
||||
|
||||
Rc::new(webview.build().unwrap())
|
||||
}
|
||||
+47
-11
@@ -1,22 +1,58 @@
|
||||
# Common hooks for Dioxus
|
||||
# Dioxus Hooks
|
||||
|
||||
This crate includes some basic useful hooks for dioxus:
|
||||
|
||||
[![Crates.io][crates-badge]][crates-url]
|
||||
[![MIT licensed][mit-badge]][mit-url]
|
||||
[![Build Status][actions-badge]][actions-url]
|
||||
[![Discord chat][discord-badge]][discord-url]
|
||||
|
||||
[crates-badge]: https://img.shields.io/crates/v/dioxus-hooks.svg
|
||||
[crates-url]: https://crates.io/crates/dioxus-hooks
|
||||
|
||||
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||
[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
|
||||
|
||||
[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
|
||||
[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster
|
||||
|
||||
[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square
|
||||
[discord-url]: https://discord.gg/XgGxMSkvUM
|
||||
|
||||
[Website](https://dioxuslabs.com) |
|
||||
[Guides](https://dioxuslabs.com/guide/) |
|
||||
[API Docs](https://docs.rs/dioxus-hooks/latest/dioxus-hooks) |
|
||||
[Chat](https://discord.gg/XgGxMSkvUM)
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
`dioxus-hooks` includes some basic useful hooks for dioxus:
|
||||
|
||||
- use_state
|
||||
- use_ref
|
||||
- use_future
|
||||
- use_coroutine
|
||||
- use_callback
|
||||
|
||||
## use_state
|
||||
Unlike React, none of these hooks are foundational since they all build off the primitive `cx.use_hook`.
|
||||
|
||||
The primary mechanism of stored state.
|
||||
This crate also provides a few helpful macros to get around some Rust lifetime management issues in async.
|
||||
|
||||
You can always use it "normally" with the `split` method:
|
||||
- `to_owned![]`
|
||||
- `use_future!()`
|
||||
- `use_callback!()`
|
||||
|
||||
```rust
|
||||
// Rusty-smart-pointer usage:
|
||||
let value = use_state(cx, || 10);
|
||||
|
||||
// "Classic" usage:
|
||||
let (value, set_value) = use_state(cx, || 0).split();
|
||||
```
|
||||
## Contributing
|
||||
|
||||
- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
|
||||
- Join the discord and ask questions!
|
||||
|
||||
## License
|
||||
This project is licensed under the [MIT license].
|
||||
|
||||
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in Dioxus by you, shall be licensed as MIT, without any additional
|
||||
terms or conditions.
|
||||
+38
-27
@@ -1,4 +1,30 @@
|
||||
# Html (and SVG) Namespace for Dioxus
|
||||
# `dioxus-html`: Html (and SVG) Namespace for Dioxus
|
||||
|
||||
|
||||
[![Crates.io][crates-badge]][crates-url]
|
||||
[![MIT licensed][mit-badge]][mit-url]
|
||||
[![Build Status][actions-badge]][actions-url]
|
||||
[![Discord chat][discord-badge]][discord-url]
|
||||
|
||||
[crates-badge]: https://img.shields.io/crates/v/dioxus-html.svg
|
||||
[crates-url]: https://crates.io/crates/dioxus-html
|
||||
|
||||
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||
[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
|
||||
|
||||
[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
|
||||
[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster
|
||||
|
||||
[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square
|
||||
[discord-url]: https://discord.gg/XgGxMSkvUM
|
||||
|
||||
[Website](https://dioxuslabs.com) |
|
||||
[Guides](https://dioxuslabs.com/guide/) |
|
||||
[API Docs](https://docs.rs/dioxus-html/latest/dioxus-html) |
|
||||
[Chat](https://discord.gg/XgGxMSkvUM)
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
The Dioxus `rsx!` and `html!` macros can accept any compile-time correct namespace on top of NodeFactory. This crate provides the HTML (and SVG) namespaces which get imported in the Dioxus prelude.
|
||||
|
||||
@@ -73,35 +99,20 @@ mod dioxus_elements {
|
||||
}
|
||||
```
|
||||
|
||||
## Limitations:
|
||||
-
|
||||
|
||||
## How to work around it:
|
||||
If an attribute in Dioxus is invalid (defined incorrectly) - first, make an issue - but then, you can work around it. The raw builder API is actually somewhat ergonomic to work with, and the NodeFactory type exposes a bunch of methods to make any type of tree - even invalid ones! So obviously, be careful, but there's basically anything you can do.
|
||||
|
||||
```rust
|
||||
cx.render(rsx!{
|
||||
div {
|
||||
h1 {}
|
||||
// Oh no! I need a super custom element
|
||||
{LazyNodes::new(move |f| {
|
||||
f.raw_element(
|
||||
// tag name
|
||||
"custom_element",
|
||||
|
||||
// attributes
|
||||
&[f.attr("billy", format_args!("goat"))],
|
||||
|
||||
// listeners
|
||||
&[f.listener(onclick(move |_| {}))],
|
||||
## Contributing
|
||||
|
||||
// children
|
||||
&[cx.render(rsx!(div {} ))],
|
||||
- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
|
||||
- Join the discord and ask questions!
|
||||
|
||||
// key
|
||||
None
|
||||
)
|
||||
})}
|
||||
}
|
||||
})
|
||||
```
|
||||
## License
|
||||
This project is licensed under the [MIT license].
|
||||
|
||||
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in Dioxus by you, shall be licensed as MIT, without any additional
|
||||
terms or conditions.
|
||||
@@ -9,7 +9,7 @@ pub type DragEvent = Event<DragData>;
|
||||
/// (such as another DOM element). Applications are free to interpret a drag and drop interaction in an
|
||||
/// application-specific way.
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct DragData {
|
||||
/// Inherit mouse data
|
||||
pub mouse: MouseData,
|
||||
|
||||
@@ -10,7 +10,7 @@ pub type MouseEvent = Event<MouseData>;
|
||||
|
||||
/// A synthetic event that wraps a web-style [`MouseEvent`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent)
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Clone, Default, PartialEq)]
|
||||
#[derive(Clone, Default, PartialEq, Eq)]
|
||||
/// Data associated with a mouse event
|
||||
///
|
||||
/// Do not use the deprecated fields; they may change or become private in the future.
|
||||
|
||||
@@ -1,7 +1,46 @@
|
||||
# JS Interpreter
|
||||
# `dioxus-interpreter-js`
|
||||
|
||||
After diffing old and new trees, the Dioxus VirtualDom produces patches that are used to modify the existing Dom. We can send these patches anywhere - including targets without WASM support.
|
||||
|
||||
In renderers with support for JavaScript, we use the interpreter from this repository - written in TypeScript - to patch the Dom. This lets us circumvent any overhead on the Rust <-> Dom boundary and keep consistency in our interpreter implementation in web/webview targets.
|
||||
[![Crates.io][crates-badge]][crates-url]
|
||||
[![MIT licensed][mit-badge]][mit-url]
|
||||
[![Build Status][actions-badge]][actions-url]
|
||||
[![Discord chat][discord-badge]][discord-url]
|
||||
|
||||
For now - both Dioxus Web and Dioxus Desktop (webview) use the same interpreter code with tweaks.
|
||||
[crates-badge]: https://img.shields.io/crates/v/dioxus-interpreter-js.svg
|
||||
[crates-url]: https://crates.io/crates/dioxus-interpreter-js
|
||||
|
||||
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||
[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
|
||||
|
||||
[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
|
||||
[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster
|
||||
|
||||
[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square
|
||||
[discord-url]: https://discord.gg/XgGxMSkvUM
|
||||
|
||||
[Website](https://dioxuslabs.com) |
|
||||
[Guides](https://dioxuslabs.com/guide/) |
|
||||
[API Docs](https://docs.rs/dioxus-interpreter-js/latest/dioxus-interpreter-js) |
|
||||
[Chat](https://discord.gg/XgGxMSkvUM)
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
`dioxus-interpreter-js` provides the high-performance JavaScript glue that interprets the stream of edits produced by the Dioxus VirtualDom and converts them into mutations on the actual web DOM.
|
||||
|
||||
This crate features bindings for the web and sledgehammer for increased performance.
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
|
||||
- Join the discord and ask questions!
|
||||
|
||||
## License
|
||||
This project is licensed under the [MIT license].
|
||||
|
||||
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in Dioxus by you, shall be licensed as MIT, without any additional
|
||||
terms or conditions.
|
||||
@@ -1,944 +0,0 @@
|
||||
export function main() {
|
||||
let root = window.document.getElementById("main");
|
||||
if (root != null) {
|
||||
window.interpreter = new Interpreter(root);
|
||||
window.ipc.postMessage(serializeIpcMessage("initialize"));
|
||||
}
|
||||
}
|
||||
|
||||
class ListenerMap {
|
||||
constructor(root) {
|
||||
// bubbling events can listen at the root element
|
||||
this.global = {};
|
||||
// non bubbling events listen at the element the listener was created at
|
||||
this.local = {};
|
||||
this.root = root;
|
||||
}
|
||||
|
||||
create(event_name, element, handler, bubbles) {
|
||||
if (bubbles) {
|
||||
if (this.global[event_name] === undefined) {
|
||||
this.global[event_name] = {};
|
||||
this.global[event_name].active = 1;
|
||||
this.global[event_name].callback = handler;
|
||||
this.root.addEventListener(event_name, handler);
|
||||
} else {
|
||||
this.global[event_name].active++;
|
||||
}
|
||||
}
|
||||
else {
|
||||
const id = element.getAttribute("data-dioxus-id");
|
||||
if (!this.local[id]) {
|
||||
this.local[id] = {};
|
||||
}
|
||||
this.local[id][event_name] = handler;
|
||||
element.addEventListener(event_name, handler);
|
||||
}
|
||||
}
|
||||
|
||||
remove(element, event_name, bubbles) {
|
||||
if (bubbles) {
|
||||
this.global[event_name].active--;
|
||||
if (this.global[event_name].active === 0) {
|
||||
this.root.removeEventListener(event_name, this.global[event_name].callback);
|
||||
delete this.global[event_name];
|
||||
}
|
||||
}
|
||||
else {
|
||||
const id = element.getAttribute("data-dioxus-id");
|
||||
delete this.local[id][event_name];
|
||||
if (this.local[id].length === 0) {
|
||||
delete this.local[id];
|
||||
}
|
||||
element.removeEventListener(event_name, handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Interpreter {
|
||||
constructor(root) {
|
||||
this.root = root;
|
||||
this.lastNode = root;
|
||||
this.listeners = new ListenerMap(root);
|
||||
this.handlers = {};
|
||||
this.nodes = [root];
|
||||
this.parents = [];
|
||||
}
|
||||
checkAppendParent() {
|
||||
if (this.parents.length > 0) {
|
||||
const lastParent = this.parents[this.parents.length - 1];
|
||||
lastParent[1]--;
|
||||
if (lastParent[1] === 0) {
|
||||
this.parents.pop();
|
||||
}
|
||||
lastParent[0].appendChild(this.lastNode);
|
||||
}
|
||||
}
|
||||
AppendChildren(root, children) {
|
||||
let node;
|
||||
if (root == null) {
|
||||
node = this.lastNode;
|
||||
} else {
|
||||
node = this.nodes[root];
|
||||
}
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
node.appendChild(this.nodes[children[i]]);
|
||||
}
|
||||
}
|
||||
ReplaceWith(root, nodes) {
|
||||
let node;
|
||||
if (root == null) {
|
||||
node = this.lastNode;
|
||||
} else {
|
||||
node = this.nodes[root];
|
||||
}
|
||||
let els = [];
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
els.push(this.nodes[nodes[i]])
|
||||
}
|
||||
node.replaceWith(...els);
|
||||
}
|
||||
InsertAfter(root, nodes) {
|
||||
let node;
|
||||
if (root == null) {
|
||||
node = this.lastNode;
|
||||
} else {
|
||||
node = this.nodes[root];
|
||||
}
|
||||
let els = [];
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
els.push(this.nodes[nodes[i]])
|
||||
}
|
||||
node.after(...els);
|
||||
}
|
||||
InsertBefore(root, nodes) {
|
||||
let node;
|
||||
if (root == null) {
|
||||
node = this.lastNode;
|
||||
} else {
|
||||
node = this.nodes[root];
|
||||
}
|
||||
let els = [];
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
els.push(this.nodes[nodes[i]])
|
||||
}
|
||||
node.before(...els);
|
||||
}
|
||||
Remove(root) {
|
||||
let node;
|
||||
if (root == null) {
|
||||
node = this.lastNode;
|
||||
} else {
|
||||
node = this.nodes[root];
|
||||
}
|
||||
if (node !== undefined) {
|
||||
node.remove();
|
||||
}
|
||||
}
|
||||
CreateTextNode(text, root) {
|
||||
this.lastNode = document.createTextNode(text);
|
||||
this.checkAppendParent();
|
||||
if (root != null) {
|
||||
this.nodes[root] = this.lastNode;
|
||||
}
|
||||
}
|
||||
CreateElement(tag, root, children) {
|
||||
this.lastNode = document.createElement(tag);
|
||||
this.checkAppendParent();
|
||||
if (root != null) {
|
||||
this.nodes[root] = this.lastNode;
|
||||
}
|
||||
if (children > 0) {
|
||||
this.parents.push([this.lastNode, children]);
|
||||
}
|
||||
}
|
||||
CreateElementNs(tag, root, ns, children) {
|
||||
this.lastNode = document.createElementNS(ns, tag);
|
||||
this.checkAppendParent();
|
||||
if (root != null) {
|
||||
this.nodes[root] = this.lastNode;
|
||||
}
|
||||
if (children > 0) {
|
||||
this.parents.push([this.lastNode, children]);
|
||||
}
|
||||
}
|
||||
CreatePlaceholder(root) {
|
||||
this.lastNode = document.createElement("pre");
|
||||
this.lastNode.hidden = true;
|
||||
this.checkAppendParent();
|
||||
if (root != null) {
|
||||
this.nodes[root] = this.lastNode;
|
||||
}
|
||||
}
|
||||
NewEventListener(event_name, root, handler, bubbles) {
|
||||
let node;
|
||||
if (root == null) {
|
||||
node = this.lastNode;
|
||||
} else {
|
||||
node = this.nodes[root];
|
||||
}
|
||||
node.setAttribute("data-dioxus-id", `${root}`);
|
||||
this.listeners.create(event_name, node, handler, bubbles);
|
||||
}
|
||||
RemoveEventListener(root, event_name, bubbles) {
|
||||
let node;
|
||||
if (root == null) {
|
||||
node = this.lastNode;
|
||||
} else {
|
||||
node = this.nodes[root];
|
||||
}
|
||||
node.removeAttribute(`data-dioxus-id`);
|
||||
this.listeners.remove(node, event_name, bubbles);
|
||||
}
|
||||
SetText(root, text) {
|
||||
let node;
|
||||
if (root == null) {
|
||||
node = this.lastNode;
|
||||
} else {
|
||||
node = this.nodes[root];
|
||||
}
|
||||
node.data = text;
|
||||
}
|
||||
SetAttribute(root, field, value, ns) {
|
||||
const name = field;
|
||||
let node;
|
||||
if (root == null) {
|
||||
node = this.lastNode;
|
||||
} else {
|
||||
node = this.nodes[root];
|
||||
}
|
||||
if (ns === "style") {
|
||||
// @ts-ignore
|
||||
node.style[name] = value;
|
||||
} else if (ns != null || ns != undefined) {
|
||||
node.setAttributeNS(ns, name, value);
|
||||
} else {
|
||||
switch (name) {
|
||||
case "value":
|
||||
if (value !== node.value) {
|
||||
node.value = value;
|
||||
}
|
||||
break;
|
||||
case "checked":
|
||||
node.checked = value === "true";
|
||||
break;
|
||||
case "selected":
|
||||
node.selected = value === "true";
|
||||
break;
|
||||
case "dangerous_inner_html":
|
||||
node.innerHTML = value;
|
||||
break;
|
||||
default:
|
||||
// https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364
|
||||
if (value === "false" && bool_attrs.hasOwnProperty(name)) {
|
||||
node.removeAttribute(name);
|
||||
} else {
|
||||
node.setAttribute(name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
RemoveAttribute(root, field, ns) {
|
||||
const name = field;
|
||||
let node;
|
||||
if (root == null) {
|
||||
node = this.lastNode;
|
||||
} else {
|
||||
node = this.nodes[root];
|
||||
}
|
||||
if (ns == "style") {
|
||||
node.style.removeProperty(name);
|
||||
} else if (ns !== null || ns !== undefined) {
|
||||
node.removeAttributeNS(ns, name);
|
||||
} else if (name === "value") {
|
||||
node.value = "";
|
||||
} else if (name === "checked") {
|
||||
node.checked = false;
|
||||
} else if (name === "selected") {
|
||||
node.selected = false;
|
||||
} else if (name === "dangerous_inner_html") {
|
||||
node.innerHTML = "";
|
||||
} else {
|
||||
node.removeAttribute(name);
|
||||
}
|
||||
}
|
||||
CloneNode(old, new_id) {
|
||||
let node;
|
||||
if (old === null) {
|
||||
node = this.lastNode;
|
||||
} else {
|
||||
node = this.nodes[old];
|
||||
}
|
||||
this.nodes[new_id] = node.cloneNode(true);
|
||||
}
|
||||
CloneNodeChildren(old, new_ids) {
|
||||
let node;
|
||||
if (old === null) {
|
||||
node = this.lastNode;
|
||||
} else {
|
||||
node = this.nodes[old];
|
||||
}
|
||||
const old_node = node.cloneNode(true);
|
||||
let i = 0;
|
||||
for (let node = old_node.firstChild; i < new_ids.length; node = node.nextSibling) {
|
||||
this.nodes[new_ids[i++]] = node;
|
||||
}
|
||||
}
|
||||
FirstChild() {
|
||||
this.lastNode = this.lastNode.firstChild;
|
||||
}
|
||||
NextSibling() {
|
||||
this.lastNode = this.lastNode.nextSibling;
|
||||
}
|
||||
ParentNode() {
|
||||
this.lastNode = this.lastNode.parentNode;
|
||||
}
|
||||
StoreWithId(id) {
|
||||
this.nodes[id] = this.lastNode;
|
||||
}
|
||||
SetLastNode(root) {
|
||||
this.lastNode = this.nodes[root];
|
||||
}
|
||||
handleEdits(edits) {
|
||||
for (let edit of edits) {
|
||||
this.handleEdit(edit);
|
||||
}
|
||||
}
|
||||
handleEdit(edit) {
|
||||
switch (edit.type) {
|
||||
case "PushRoot":
|
||||
this.PushRoot(edit.root);
|
||||
break;
|
||||
case "AppendChildren":
|
||||
this.AppendChildren(edit.root, edit.children);
|
||||
break;
|
||||
case "ReplaceWith":
|
||||
this.ReplaceWith(edit.root, edit.nodes);
|
||||
break;
|
||||
case "InsertAfter":
|
||||
this.InsertAfter(edit.root, edit.nodes);
|
||||
break;
|
||||
case "InsertBefore":
|
||||
this.InsertBefore(edit.root, edit.nodes);
|
||||
break;
|
||||
case "Remove":
|
||||
this.Remove(edit.root);
|
||||
break;
|
||||
case "CreateTextNode":
|
||||
this.CreateTextNode(edit.text, edit.root);
|
||||
break;
|
||||
case "CreateElement":
|
||||
this.CreateElement(edit.tag, edit.root, edit.children);
|
||||
break;
|
||||
case "CreateElementNs":
|
||||
this.CreateElementNs(edit.tag, edit.root, edit.ns, edit.children);
|
||||
break;
|
||||
case "CreatePlaceholder":
|
||||
this.CreatePlaceholder(edit.root);
|
||||
break;
|
||||
case "RemoveEventListener":
|
||||
this.RemoveEventListener(edit.root, edit.event_name);
|
||||
break;
|
||||
case "NewEventListener":
|
||||
// this handler is only provided on desktop implementations since this
|
||||
// method is not used by the web implementation
|
||||
let handler = (event) => {
|
||||
let target = event.target;
|
||||
if (target != null) {
|
||||
let realId = target.getAttribute(`data-dioxus-id`);
|
||||
let shouldPreventDefault = target.getAttribute(
|
||||
`dioxus-prevent-default`
|
||||
);
|
||||
|
||||
if (event.type === "click") {
|
||||
// todo call prevent default if it's the right type of event
|
||||
if (shouldPreventDefault !== `onclick`) {
|
||||
if (target.tagName === "A") {
|
||||
event.preventDefault();
|
||||
const href = target.getAttribute("href");
|
||||
if (href !== "" && href !== null && href !== undefined) {
|
||||
window.ipc.postMessage(
|
||||
serializeIpcMessage("browser_open", { href })
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// also prevent buttons from submitting
|
||||
if (target.tagName === "BUTTON" && event.type == "submit") {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
// walk the tree to find the real element
|
||||
while (realId == null) {
|
||||
// we've reached the root we don't want to send an event
|
||||
if (target.parentElement === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
target = target.parentElement;
|
||||
realId = target.getAttribute(`data-dioxus-id`);
|
||||
}
|
||||
|
||||
shouldPreventDefault = target.getAttribute(
|
||||
`dioxus-prevent-default`
|
||||
);
|
||||
|
||||
let contents = serialize_event(event);
|
||||
|
||||
if (shouldPreventDefault === `on${event.type}`) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
if (event.type === "submit") {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
if (
|
||||
target.tagName === "FORM" &&
|
||||
(event.type === "submit" || event.type === "input")
|
||||
) {
|
||||
for (let x = 0; x < target.elements.length; x++) {
|
||||
let element = target.elements[x];
|
||||
let name = element.getAttribute("name");
|
||||
if (name != null) {
|
||||
if (element.getAttribute("type") === "checkbox") {
|
||||
// @ts-ignore
|
||||
contents.values[name] = element.checked ? "true" : "false";
|
||||
} else if (element.getAttribute("type") === "radio") {
|
||||
if (element.checked) {
|
||||
contents.values[name] = element.value;
|
||||
}
|
||||
} else {
|
||||
// @ts-ignore
|
||||
contents.values[name] =
|
||||
element.value ?? element.textContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (realId === null) {
|
||||
return;
|
||||
}
|
||||
realId = parseInt(realId);
|
||||
window.ipc.postMessage(
|
||||
serializeIpcMessage("user_event", {
|
||||
event: edit.event_name,
|
||||
mounted_dom_id: realId,
|
||||
contents: contents,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
this.NewEventListener(edit.event_name, edit.root, handler, event_bubbles(edit.event_name));
|
||||
|
||||
break;
|
||||
case "SetText":
|
||||
this.SetText(edit.root, edit.text);
|
||||
break;
|
||||
case "SetAttribute":
|
||||
this.SetAttribute(edit.root, edit.field, edit.value, edit.ns);
|
||||
break;
|
||||
case "RemoveAttribute":
|
||||
this.RemoveAttribute(edit.root, edit.name, edit.ns);
|
||||
break;
|
||||
case "CloneNode":
|
||||
this.CloneNode(edit.id, edit.new_id);
|
||||
break;
|
||||
case "CloneNodeChildren":
|
||||
this.CloneNodeChildren(edit.id, edit.new_ids);
|
||||
break;
|
||||
case "FirstChild":
|
||||
this.FirstChild();
|
||||
break;
|
||||
case "NextSibling":
|
||||
this.NextSibling();
|
||||
break;
|
||||
case "ParentNode":
|
||||
this.ParentNode();
|
||||
break;
|
||||
case "StoreWithId":
|
||||
this.StoreWithId(BigInt(edit.id));
|
||||
break;
|
||||
case "SetLastNode":
|
||||
this.SetLastNode(BigInt(edit.id));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function serialize_event(event) {
|
||||
switch (event.type) {
|
||||
case "copy":
|
||||
case "cut":
|
||||
case "past": {
|
||||
return {};
|
||||
}
|
||||
case "compositionend":
|
||||
case "compositionstart":
|
||||
case "compositionupdate": {
|
||||
let { data } = event;
|
||||
return {
|
||||
data,
|
||||
};
|
||||
}
|
||||
case "keydown":
|
||||
case "keypress":
|
||||
case "keyup": {
|
||||
let {
|
||||
charCode,
|
||||
key,
|
||||
altKey,
|
||||
ctrlKey,
|
||||
metaKey,
|
||||
keyCode,
|
||||
shiftKey,
|
||||
location,
|
||||
repeat,
|
||||
which,
|
||||
code,
|
||||
} = event;
|
||||
return {
|
||||
char_code: charCode,
|
||||
key: key,
|
||||
alt_key: altKey,
|
||||
ctrl_key: ctrlKey,
|
||||
meta_key: metaKey,
|
||||
key_code: keyCode,
|
||||
shift_key: shiftKey,
|
||||
location: location,
|
||||
repeat: repeat,
|
||||
which: which,
|
||||
code,
|
||||
};
|
||||
}
|
||||
case "focus":
|
||||
case "blur": {
|
||||
return {};
|
||||
}
|
||||
case "change": {
|
||||
let target = event.target;
|
||||
let value;
|
||||
if (target.type === "checkbox" || target.type === "radio") {
|
||||
value = target.checked ? "true" : "false";
|
||||
} else {
|
||||
value = target.value ?? target.textContent;
|
||||
}
|
||||
return {
|
||||
value: value,
|
||||
values: {},
|
||||
};
|
||||
}
|
||||
case "input":
|
||||
case "invalid":
|
||||
case "reset":
|
||||
case "submit": {
|
||||
let target = event.target;
|
||||
let value = target.value ?? target.textContent;
|
||||
|
||||
if (target.type === "checkbox") {
|
||||
value = target.checked ? "true" : "false";
|
||||
}
|
||||
|
||||
return {
|
||||
value: value,
|
||||
values: {},
|
||||
};
|
||||
}
|
||||
case "click":
|
||||
case "contextmenu":
|
||||
case "doubleclick":
|
||||
case "dblclick":
|
||||
case "drag":
|
||||
case "dragend":
|
||||
case "dragenter":
|
||||
case "dragexit":
|
||||
case "dragleave":
|
||||
case "dragover":
|
||||
case "dragstart":
|
||||
case "drop":
|
||||
case "mousedown":
|
||||
case "mouseenter":
|
||||
case "mouseleave":
|
||||
case "mousemove":
|
||||
case "mouseout":
|
||||
case "mouseover":
|
||||
case "mouseup": {
|
||||
const {
|
||||
altKey,
|
||||
button,
|
||||
buttons,
|
||||
clientX,
|
||||
clientY,
|
||||
ctrlKey,
|
||||
metaKey,
|
||||
offsetX,
|
||||
offsetY,
|
||||
pageX,
|
||||
pageY,
|
||||
screenX,
|
||||
screenY,
|
||||
shiftKey,
|
||||
} = event;
|
||||
return {
|
||||
alt_key: altKey,
|
||||
button: button,
|
||||
buttons: buttons,
|
||||
client_x: clientX,
|
||||
client_y: clientY,
|
||||
ctrl_key: ctrlKey,
|
||||
meta_key: metaKey,
|
||||
offset_x: offsetX,
|
||||
offset_y: offsetY,
|
||||
page_x: pageX,
|
||||
page_y: pageY,
|
||||
screen_x: screenX,
|
||||
screen_y: screenY,
|
||||
shift_key: shiftKey,
|
||||
};
|
||||
}
|
||||
case "pointerdown":
|
||||
case "pointermove":
|
||||
case "pointerup":
|
||||
case "pointercancel":
|
||||
case "gotpointercapture":
|
||||
case "lostpointercapture":
|
||||
case "pointerenter":
|
||||
case "pointerleave":
|
||||
case "pointerover":
|
||||
case "pointerout": {
|
||||
const {
|
||||
altKey,
|
||||
button,
|
||||
buttons,
|
||||
clientX,
|
||||
clientY,
|
||||
ctrlKey,
|
||||
metaKey,
|
||||
pageX,
|
||||
pageY,
|
||||
screenX,
|
||||
screenY,
|
||||
shiftKey,
|
||||
pointerId,
|
||||
width,
|
||||
height,
|
||||
pressure,
|
||||
tangentialPressure,
|
||||
tiltX,
|
||||
tiltY,
|
||||
twist,
|
||||
pointerType,
|
||||
isPrimary,
|
||||
} = event;
|
||||
return {
|
||||
alt_key: altKey,
|
||||
button: button,
|
||||
buttons: buttons,
|
||||
client_x: clientX,
|
||||
client_y: clientY,
|
||||
ctrl_key: ctrlKey,
|
||||
meta_key: metaKey,
|
||||
page_x: pageX,
|
||||
page_y: pageY,
|
||||
screen_x: screenX,
|
||||
screen_y: screenY,
|
||||
shift_key: shiftKey,
|
||||
pointer_id: pointerId,
|
||||
width: width,
|
||||
height: height,
|
||||
pressure: pressure,
|
||||
tangential_pressure: tangentialPressure,
|
||||
tilt_x: tiltX,
|
||||
tilt_y: tiltY,
|
||||
twist: twist,
|
||||
pointer_type: pointerType,
|
||||
is_primary: isPrimary,
|
||||
};
|
||||
}
|
||||
case "select": {
|
||||
return {};
|
||||
}
|
||||
case "touchcancel":
|
||||
case "touchend":
|
||||
case "touchmove":
|
||||
case "touchstart": {
|
||||
const { altKey, ctrlKey, metaKey, shiftKey } = event;
|
||||
return {
|
||||
// changed_touches: event.changedTouches,
|
||||
// target_touches: event.targetTouches,
|
||||
// touches: event.touches,
|
||||
alt_key: altKey,
|
||||
ctrl_key: ctrlKey,
|
||||
meta_key: metaKey,
|
||||
shift_key: shiftKey,
|
||||
};
|
||||
}
|
||||
case "scroll": {
|
||||
return {};
|
||||
}
|
||||
case "wheel": {
|
||||
const { deltaX, deltaY, deltaZ, deltaMode } = event;
|
||||
return {
|
||||
delta_x: deltaX,
|
||||
delta_y: deltaY,
|
||||
delta_z: deltaZ,
|
||||
delta_mode: deltaMode,
|
||||
};
|
||||
}
|
||||
case "animationstart":
|
||||
case "animationend":
|
||||
case "animationiteration": {
|
||||
const { animationName, elapsedTime, pseudoElement } = event;
|
||||
return {
|
||||
animation_name: animationName,
|
||||
elapsed_time: elapsedTime,
|
||||
pseudo_element: pseudoElement,
|
||||
};
|
||||
}
|
||||
case "transitionend": {
|
||||
const { propertyName, elapsedTime, pseudoElement } = event;
|
||||
return {
|
||||
property_name: propertyName,
|
||||
elapsed_time: elapsedTime,
|
||||
pseudo_element: pseudoElement,
|
||||
};
|
||||
}
|
||||
case "abort":
|
||||
case "canplay":
|
||||
case "canplaythrough":
|
||||
case "durationchange":
|
||||
case "emptied":
|
||||
case "encrypted":
|
||||
case "ended":
|
||||
case "error":
|
||||
case "loadeddata":
|
||||
case "loadedmetadata":
|
||||
case "loadstart":
|
||||
case "pause":
|
||||
case "play":
|
||||
case "playing":
|
||||
case "progress":
|
||||
case "ratechange":
|
||||
case "seeked":
|
||||
case "seeking":
|
||||
case "stalled":
|
||||
case "suspend":
|
||||
case "timeupdate":
|
||||
case "volumechange":
|
||||
case "waiting": {
|
||||
return {};
|
||||
}
|
||||
case "toggle": {
|
||||
return {};
|
||||
}
|
||||
default: {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
function serializeIpcMessage(method, params = {}) {
|
||||
return JSON.stringify({ method, params });
|
||||
}
|
||||
const bool_attrs = {
|
||||
allowfullscreen: true,
|
||||
allowpaymentrequest: true,
|
||||
async: true,
|
||||
autofocus: true,
|
||||
autoplay: true,
|
||||
checked: true,
|
||||
controls: true,
|
||||
default: true,
|
||||
defer: true,
|
||||
disabled: true,
|
||||
formnovalidate: true,
|
||||
hidden: true,
|
||||
ismap: true,
|
||||
itemscope: true,
|
||||
loop: true,
|
||||
multiple: true,
|
||||
muted: true,
|
||||
nomodule: true,
|
||||
novalidate: true,
|
||||
open: true,
|
||||
playsinline: true,
|
||||
readonly: true,
|
||||
required: true,
|
||||
reversed: true,
|
||||
selected: true,
|
||||
truespeed: true,
|
||||
};
|
||||
|
||||
function is_element_node(node) {
|
||||
return node.nodeType == 1;
|
||||
}
|
||||
|
||||
function event_bubbles(event) {
|
||||
switch (event) {
|
||||
case "copy":
|
||||
return true;
|
||||
case "cut":
|
||||
return true;
|
||||
case "paste":
|
||||
return true;
|
||||
case "compositionend":
|
||||
return true;
|
||||
case "compositionstart":
|
||||
return true;
|
||||
case "compositionupdate":
|
||||
return true;
|
||||
case "keydown":
|
||||
return true;
|
||||
case "keypress":
|
||||
return true;
|
||||
case "keyup":
|
||||
return true;
|
||||
case "focus":
|
||||
return false;
|
||||
case "focusout":
|
||||
return true;
|
||||
case "focusin":
|
||||
return true;
|
||||
case "blur":
|
||||
return false;
|
||||
case "change":
|
||||
return true;
|
||||
case "input":
|
||||
return true;
|
||||
case "invalid":
|
||||
return true;
|
||||
case "reset":
|
||||
return true;
|
||||
case "submit":
|
||||
return true;
|
||||
case "click":
|
||||
return true;
|
||||
case "contextmenu":
|
||||
return true;
|
||||
case "doubleclick":
|
||||
return true;
|
||||
case "dblclick":
|
||||
return true;
|
||||
case "drag":
|
||||
return true;
|
||||
case "dragend":
|
||||
return true;
|
||||
case "dragenter":
|
||||
return false;
|
||||
case "dragexit":
|
||||
return false;
|
||||
case "dragleave":
|
||||
return true;
|
||||
case "dragover":
|
||||
return true;
|
||||
case "dragstart":
|
||||
return true;
|
||||
case "drop":
|
||||
return true;
|
||||
case "mousedown":
|
||||
return true;
|
||||
case "mouseenter":
|
||||
return false;
|
||||
case "mouseleave":
|
||||
return false;
|
||||
case "mousemove":
|
||||
return true;
|
||||
case "mouseout":
|
||||
return true;
|
||||
case "scroll":
|
||||
return false;
|
||||
case "mouseover":
|
||||
return true;
|
||||
case "mouseup":
|
||||
return true;
|
||||
case "pointerdown":
|
||||
return true;
|
||||
case "pointermove":
|
||||
return true;
|
||||
case "pointerup":
|
||||
return true;
|
||||
case "pointercancel":
|
||||
return true;
|
||||
case "gotpointercapture":
|
||||
return true;
|
||||
case "lostpointercapture":
|
||||
return true;
|
||||
case "pointerenter":
|
||||
return false;
|
||||
case "pointerleave":
|
||||
return false;
|
||||
case "pointerover":
|
||||
return true;
|
||||
case "pointerout":
|
||||
return true;
|
||||
case "select":
|
||||
return true;
|
||||
case "touchcancel":
|
||||
return true;
|
||||
case "touchend":
|
||||
return true;
|
||||
case "touchmove":
|
||||
return true;
|
||||
case "touchstart":
|
||||
return true;
|
||||
case "wheel":
|
||||
return true;
|
||||
case "abort":
|
||||
return false;
|
||||
case "canplay":
|
||||
return false;
|
||||
case "canplaythrough":
|
||||
return false;
|
||||
case "durationchange":
|
||||
return false;
|
||||
case "emptied":
|
||||
return false;
|
||||
case "encrypted":
|
||||
return true;
|
||||
case "ended":
|
||||
return false;
|
||||
case "error":
|
||||
return false;
|
||||
case "loadeddata":
|
||||
return false;
|
||||
case "loadedmetadata":
|
||||
return false;
|
||||
case "loadstart":
|
||||
return false;
|
||||
case "pause":
|
||||
return false;
|
||||
case "play":
|
||||
return false;
|
||||
case "playing":
|
||||
return false;
|
||||
case "progress":
|
||||
return false;
|
||||
case "ratechange":
|
||||
return false;
|
||||
case "seeked":
|
||||
return false;
|
||||
case "seeking":
|
||||
return false;
|
||||
case "stalled":
|
||||
return false;
|
||||
case "suspend":
|
||||
return false;
|
||||
case "timeupdate":
|
||||
return false;
|
||||
case "volumechange":
|
||||
return false;
|
||||
case "waiting":
|
||||
return false;
|
||||
case "animationstart":
|
||||
return true;
|
||||
case "animationend":
|
||||
return true;
|
||||
case "animationiteration":
|
||||
return true;
|
||||
case "transitionend":
|
||||
return true;
|
||||
case "toggle":
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,55 @@
|
||||
# Dioxus LiveView
|
||||
# Dioxus Liveview
|
||||
|
||||
Server rendered apps with minimal latency
|
||||
|
||||
[![Crates.io][crates-badge]][crates-url]
|
||||
[![MIT licensed][mit-badge]][mit-url]
|
||||
[![Build Status][actions-badge]][actions-url]
|
||||
[![Discord chat][discord-badge]][discord-url]
|
||||
|
||||
[crates-badge]: https://img.shields.io/crates/v/dioxus-liveview.svg
|
||||
[crates-url]: https://crates.io/crates/dioxus-liveview
|
||||
|
||||
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||
[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
|
||||
|
||||
[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
|
||||
[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster
|
||||
|
||||
[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square
|
||||
[discord-url]: https://discord.gg/XgGxMSkvUM
|
||||
|
||||
[Website](https://dioxuslabs.com) |
|
||||
[Guides](https://dioxuslabs.com/guide/) |
|
||||
[API Docs](https://docs.rs/dioxus-liveview/latest/dioxus-liveview) |
|
||||
[Chat](https://discord.gg/XgGxMSkvUM)
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
`dioxus-liveview` provides adapters for running the Dioxus VirtualDom over a websocket connection.
|
||||
|
||||
The current backend frameworks supported include:
|
||||
|
||||
- Axum
|
||||
- Warp
|
||||
- Salvo
|
||||
|
||||
Dioxus-LiveView exports a number of primitives to wire up an app into an existing backend framework.
|
||||
|
||||
- A threadpool for spawning the `!Send` VirtualDom and interacting with it from the websocket
|
||||
- An adapter for transforming various socket types into the `LiveViewSocket` type
|
||||
- The glue to load the interpreter into your app
|
||||
|
||||
## Contributing
|
||||
|
||||
- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
|
||||
- Join the discord and ask questions!
|
||||
|
||||
## License
|
||||
This project is licensed under the [MIT license].
|
||||
|
||||
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in Dioxus by you, shall be licensed as MIT, without any additional
|
||||
terms or conditions.
|
||||
@@ -1,11 +1,34 @@
|
||||
# Getting started: mobile
|
||||
# Dioxus Mobile
|
||||
|
||||
[![Crates.io][crates-badge]][crates-url]
|
||||
[![MIT licensed][mit-badge]][mit-url]
|
||||
[![Build Status][actions-badge]][actions-url]
|
||||
[![Discord chat][discord-badge]][discord-url]
|
||||
|
||||
[crates-badge]: https://img.shields.io/crates/v/dioxus-mobile.svg
|
||||
[crates-url]: https://crates.io/crates/dioxus-mobile
|
||||
|
||||
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||
[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
|
||||
|
||||
[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
|
||||
[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster
|
||||
|
||||
[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square
|
||||
[discord-url]: https://discord.gg/XgGxMSkvUM
|
||||
|
||||
[Website](https://dioxuslabs.com) |
|
||||
[Guides](https://dioxuslabs.com/guide/) |
|
||||
[API Docs](https://docs.rs/dioxus-mobile/latest/dioxus-mobile) |
|
||||
[Chat](https://discord.gg/XgGxMSkvUM)
|
||||
|
||||
|
||||
Dioxus is unique in that it actually supports mobile. However, support is very young and you might need to dip down into some of the primitives until better supported is ready.
|
||||
## Overview
|
||||
|
||||
Currently, only iOS is supported through us, however you *can* add android support by following the same instructions below, but using the `android` guide in `cargo-mobile`.
|
||||
`dioxus-mobile` is a re-export of `dioxus-desktop` with some minor tweaks and documentation changes. As this crate evolves, it will provide some more unique features to mobile, but for now, it's very similar to the desktop crate.
|
||||
|
||||
Dioxus Mobile supports both iOS and Android. However, Android support is still quite experimental and requires a lot of configuration. A good area to contribute here would be to improve the CLI tool to include bundling and mobile configuration.
|
||||
|
||||
Also, Dioxus Desktop and Dioxus Mobile share the same codebase, and dioxus-mobile currently just re-exports dioxus-desktop.
|
||||
|
||||
## Getting Set up
|
||||
|
||||
@@ -76,3 +99,19 @@ To configure the webview, menubar, and other important desktop-specific features
|
||||
## Future Steps
|
||||
|
||||
Make sure to read the [Dioxus Guide](https://dioxuslabs.com/guide) if you already haven't!
|
||||
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
|
||||
- Join the discord and ask questions!
|
||||
|
||||
## License
|
||||
This project is licensed under the [MIT license].
|
||||
|
||||
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in Dioxus by you, shall be licensed as MIT, without any additional
|
||||
terms or conditions.
|
||||
@@ -0,0 +1,43 @@
|
||||
# Dioxus Native Core Macro
|
||||
|
||||
[![Crates.io][crates-badge]][crates-url]
|
||||
[![MIT licensed][mit-badge]][mit-url]
|
||||
[![Build Status][actions-badge]][actions-url]
|
||||
[![Discord chat][discord-badge]][discord-url]
|
||||
|
||||
[crates-badge]: https://img.shields.io/crates/v/dioxus-native-core-macro.svg
|
||||
[crates-url]: https://crates.io/crates/dioxus-native-core-macro
|
||||
|
||||
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||
[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
|
||||
|
||||
[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
|
||||
[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster
|
||||
|
||||
[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square
|
||||
[discord-url]: https://discord.gg/XgGxMSkvUM
|
||||
|
||||
[Website](https://dioxuslabs.com) |
|
||||
[Guides](https://dioxuslabs.com/guide/) |
|
||||
[API Docs](https://docs.rs/dioxus-native-core-macro/latest/dioxus-native-core-macro) |
|
||||
[Chat](https://discord.gg/XgGxMSkvUM)
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
`dioxus-native-core-macro` provides a handful of macros used by native-core for native renderers like TUI, Blitz, and Freya to derive their own state.
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
|
||||
- Join the discord and ask questions!
|
||||
|
||||
## License
|
||||
This project is licensed under the [MIT license].
|
||||
|
||||
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in Dioxus by you, shall be licensed as MIT, without any additional
|
||||
terms or conditions.
|
||||
@@ -1 +1,45 @@
|
||||
# Dioxus Native-Core: An lazy approach to GUI state and trees
|
||||
# Dioxus Native Core
|
||||
|
||||
[![Crates.io][crates-badge]][crates-url]
|
||||
[![MIT licensed][mit-badge]][mit-url]
|
||||
[![Build Status][actions-badge]][actions-url]
|
||||
[![Discord chat][discord-badge]][discord-url]
|
||||
|
||||
[crates-badge]: https://img.shields.io/crates/v/dioxus-native-core.svg
|
||||
[crates-url]: https://crates.io/crates/dioxus-native-core
|
||||
|
||||
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||
[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
|
||||
|
||||
[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
|
||||
[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster
|
||||
|
||||
[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square
|
||||
[discord-url]: https://discord.gg/XgGxMSkvUM
|
||||
|
||||
[Website](https://dioxuslabs.com) |
|
||||
[Guides](https://dioxuslabs.com/guide/) |
|
||||
[API Docs](https://docs.rs/dioxus-native-core/latest/dioxus-native-core) |
|
||||
[Chat](https://discord.gg/XgGxMSkvUM)
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
`dioxus-native-core` provides a number of helpful utilities for lazily resolving computed values of the Dioxus VirtualDom to be used in conjunction with a native rendering engine.
|
||||
|
||||
The main "value-add" of this crate over implementing your own native tree is that this tree is incrementally recomputed using the Dioxus VirtualDom's edit stream. Only parts of the tree that rely on each other will be redrawn - all else will be ignored.
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
|
||||
- Join the discord and ask questions!
|
||||
|
||||
## License
|
||||
This project is licensed under the [MIT license].
|
||||
|
||||
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in Dioxus by you, shall be licensed as MIT, without any additional
|
||||
terms or conditions.
|
||||
+39
-54
@@ -1,55 +1,31 @@
|
||||
<div align="center">
|
||||
<h1>Dioxus Router</h1>
|
||||
</div>
|
||||
# Dioxus Native Core
|
||||
|
||||
[![Crates.io][crates-badge]][crates-url]
|
||||
[![MIT licensed][mit-badge]][mit-url]
|
||||
[![Build Status][actions-badge]][actions-url]
|
||||
[![Discord chat][discord-badge]][discord-url]
|
||||
|
||||
[crates-badge]: https://img.shields.io/crates/v/dioxus-router.svg
|
||||
[crates-url]: https://crates.io/crates/dioxus-router
|
||||
|
||||
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||
[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
|
||||
|
||||
[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
|
||||
[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster
|
||||
|
||||
[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square
|
||||
[discord-url]: https://discord.gg/XgGxMSkvUM
|
||||
|
||||
[Website](https://dioxuslabs.com) |
|
||||
[Guides](https://dioxuslabs.com/guide/) |
|
||||
[API Docs](https://docs.rs/dioxus-router/latest/dioxus-router) |
|
||||
[Chat](https://discord.gg/XgGxMSkvUM)
|
||||
|
||||
|
||||
<div align="center">
|
||||
<!-- Crates version -->
|
||||
<a href="https://crates.io/crates/dioxus">
|
||||
<img src="https://img.shields.io/crates/v/dioxus.svg?style=flat-square"
|
||||
alt="Crates.io version" />
|
||||
</a>
|
||||
<!-- Downloads -->
|
||||
<a href="https://crates.io/crates/dioxus">
|
||||
<img src="https://img.shields.io/crates/d/dioxus.svg?style=flat-square"
|
||||
alt="Download" />
|
||||
</a>
|
||||
<!-- docs -->
|
||||
<a href="https://docs.rs/dioxus">
|
||||
<img src="https://img.shields.io/badge/docs-latest-blue.svg?style=flat-square"
|
||||
alt="docs.rs docs" />
|
||||
</a>
|
||||
<!-- CI -->
|
||||
<a href="https://github.com/jkelleyrtp/dioxus/actions">
|
||||
<img src="https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg"
|
||||
alt="CI status" />
|
||||
</a>
|
||||
## Overview
|
||||
|
||||
<!--Awesome -->
|
||||
<a href="https://github.com/dioxuslabs/awesome-dioxus">
|
||||
<img src="https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg" alt="Awesome Page" />
|
||||
</a>
|
||||
<!-- Discord -->
|
||||
<a href="https://discord.gg/XgGxMSkvUM">
|
||||
<img src="https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square" alt="Discord Link" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div align="center">
|
||||
<h3>
|
||||
<a href="https://dioxuslabs.com"> Website </a>
|
||||
<span> | </span>
|
||||
<a href="https://github.com/DioxusLabs/example-projects"> Examples </a>
|
||||
<span> | </span>
|
||||
<a href="https://dioxuslabs.com/router"> Guide (Latest) </a>
|
||||
<span> | </span>
|
||||
<a href="https://dioxuslabs.com/nightly/router"> Guide (Master) </a>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
Dioxus Router is a first-party Router for all your Dioxus Apps. It provides a React-Router style interface that works anywhere: across the browser, SSR, and natively.
|
||||
Dioxus Router is a first-party Router for all your Dioxus Apps. It provides a React-Router style interface using somewhat loose typing rules.
|
||||
|
||||
```rust, ignore
|
||||
fn app() {
|
||||
@@ -63,10 +39,19 @@ fn app() {
|
||||
}
|
||||
```
|
||||
|
||||
You need to enable the right features for the platform you're targeting since these are not determined automatically!
|
||||
|
||||
## Resources
|
||||
## Contributing
|
||||
|
||||
- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
|
||||
- Join the discord and ask questions!
|
||||
|
||||
## License
|
||||
This project is licensed under the [MIT license].
|
||||
|
||||
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in Dioxus by you, shall be licensed as MIT, without any additional
|
||||
terms or conditions.
|
||||
|
||||
- See the mdbook
|
||||
- See the one-page brief
|
||||
- See the guide on the doc site
|
||||
- The crates.io API
|
||||
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "rsx-rosetta"
|
||||
version = "0.0.0"
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dioxus-autofmt = { path = "../autofmt" }
|
||||
dioxus-rsx = { path = "../rsx" }
|
||||
html_parser = "0.6.3"
|
||||
proc-macro2 = "1.0.49"
|
||||
quote = "1.0.23"
|
||||
syn = { version = "1.0.107", features = ["full"] }
|
||||
convert_case = "0.5.0"
|
||||
|
||||
# [features]
|
||||
# default = ["html"]
|
||||
|
||||
# eventually more output options
|
||||
@@ -0,0 +1,59 @@
|
||||
|
||||
|
||||
# `rsx-rosetta`
|
||||
|
||||
[![Crates.io][crates-badge]][crates-url]
|
||||
[![MIT licensed][mit-badge]][mit-url]
|
||||
[![Build Status][actions-badge]][actions-url]
|
||||
[![Discord chat][discord-badge]][discord-url]
|
||||
|
||||
[crates-badge]: https://img.shields.io/crates/v/rsx-rosetta.svg
|
||||
[crates-url]: https://crates.io/crates/rsx-rosetta
|
||||
|
||||
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||
[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
|
||||
|
||||
[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
|
||||
[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster
|
||||
|
||||
[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square
|
||||
[discord-url]: https://discord.gg/XgGxMSkvUM
|
||||
|
||||
[Website](https://dioxuslabs.com) |
|
||||
[Guides](https://dioxuslabs.com/guide/) |
|
||||
[API Docs](https://docs.rs/rsx-rosetta/latest/rsx-rosetta) |
|
||||
[Chat](https://discord.gg/XgGxMSkvUM)
|
||||
|
||||
## Overview
|
||||
|
||||
Dioxus sports its own templating language inspired by C#/Kotlin/RTMP, etc. It's pretty straightforward.
|
||||
|
||||
However, it's NOT HTML. This is done since HTML is verbose and you'd need a dedicated LSP or IDE integration to get a good DX in .rs files.
|
||||
|
||||
RSX is simple... It's similar enough to regular Rust code to trick most IDEs into automatically providing support for things like block selections, folding, highlighting, etc.
|
||||
|
||||
To accomodate the transition from HTML to RSX, you might need to translate some existing code.
|
||||
|
||||
This library provids a central AST that can accept a number of inputs:
|
||||
|
||||
- HTML
|
||||
- Syn (todo)
|
||||
- Akama (todo)
|
||||
- Jinja (todo)
|
||||
|
||||
From there, you can convert directly to a string or into some other AST.
|
||||
|
||||
## Contributing
|
||||
|
||||
- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
|
||||
- Join the discord and ask questions!
|
||||
|
||||
## License
|
||||
This project is licensed under the [MIT license].
|
||||
|
||||
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in Dioxus by you, shall be licensed as MIT, without any additional
|
||||
terms or conditions.
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
use html_parser::Dom;
|
||||
|
||||
fn main() {
|
||||
let html = r#"
|
||||
<div>
|
||||
<div class="asd">hello world!</div>
|
||||
<div id="asd">hello world!</div>
|
||||
<div id="asd">hello world!</div>
|
||||
<div for="asd">hello world!</div>
|
||||
<div async="asd">hello world!</div>
|
||||
<div LargeThing="asd">hello world!</div>
|
||||
<ai-is-awesome>hello world!</ai-is-awesome>
|
||||
</div>
|
||||
"#
|
||||
.trim();
|
||||
|
||||
let dom = Dom::parse(html).unwrap();
|
||||
|
||||
let body = rsx_rosetta::rsx_from_html(&dom);
|
||||
|
||||
let out = dioxus_autofmt::write_block_out(body).unwrap();
|
||||
|
||||
println!("{}", out);
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
use convert_case::{Case, Casing};
|
||||
use dioxus_rsx::{
|
||||
BodyNode, CallBody, Component, Element, ElementAttr, ElementAttrNamed, IfmtInput,
|
||||
};
|
||||
pub use html_parser::{Dom, Node};
|
||||
use proc_macro2::{Ident, Span};
|
||||
use syn::{punctuated::Punctuated, LitStr};
|
||||
|
||||
/// Convert an HTML DOM tree into an RSX CallBody
|
||||
pub fn rsx_from_html(dom: &Dom) -> CallBody {
|
||||
CallBody {
|
||||
roots: dom.children.iter().filter_map(rsx_node_from_html).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert an HTML Node into an RSX BodyNode
|
||||
///
|
||||
/// If the node is a comment, it will be ignored since RSX doesn't support comments
|
||||
pub fn rsx_node_from_html(node: &Node) -> Option<BodyNode> {
|
||||
match node {
|
||||
Node::Text(text) => Some(BodyNode::Text(ifmt_from_text(text))),
|
||||
Node::Element(el) => {
|
||||
let el_name = el.name.to_case(Case::Snake);
|
||||
let el_name = Ident::new(el_name.as_str(), Span::call_site());
|
||||
|
||||
let mut attributes: Vec<_> = el
|
||||
.attributes
|
||||
.iter()
|
||||
.map(|(name, value)| {
|
||||
let ident = if matches!(name.as_str(), "for" | "async" | "type" | "as") {
|
||||
Ident::new_raw(name.as_str(), Span::call_site())
|
||||
} else {
|
||||
let new_name = name.to_case(Case::Snake);
|
||||
Ident::new(new_name.as_str(), Span::call_site())
|
||||
};
|
||||
|
||||
ElementAttrNamed {
|
||||
el_name: el_name.clone(),
|
||||
attr: ElementAttr::AttrText {
|
||||
value: ifmt_from_text(value.as_deref().unwrap_or("false")),
|
||||
name: ident,
|
||||
},
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let class = el.classes.join(" ");
|
||||
if !class.is_empty() {
|
||||
attributes.push(ElementAttrNamed {
|
||||
el_name: el_name.clone(),
|
||||
attr: ElementAttr::AttrText {
|
||||
name: Ident::new("class", Span::call_site()),
|
||||
value: ifmt_from_text(&class),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(id) = &el.id {
|
||||
attributes.push(ElementAttrNamed {
|
||||
el_name: el_name.clone(),
|
||||
attr: ElementAttr::AttrText {
|
||||
name: Ident::new("id", Span::call_site()),
|
||||
value: ifmt_from_text(id),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let children = el.children.iter().filter_map(rsx_node_from_html).collect();
|
||||
|
||||
Some(BodyNode::Element(Element {
|
||||
name: el_name,
|
||||
children,
|
||||
attributes,
|
||||
_is_static: false,
|
||||
key: None,
|
||||
}))
|
||||
}
|
||||
|
||||
// We ignore comments
|
||||
Node::Comment(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Pull out all the svgs from the body and replace them with components of the same name
|
||||
pub fn collect_svgs(children: &mut [BodyNode], out: &mut Vec<BodyNode>) {
|
||||
for child in children {
|
||||
match child {
|
||||
BodyNode::Component(comp) => collect_svgs(&mut comp.children, out),
|
||||
|
||||
BodyNode::Element(el) if el.name == "svg" => {
|
||||
// we want to replace this instance with a component
|
||||
let mut segments = Punctuated::new();
|
||||
|
||||
segments.push(Ident::new("icons", Span::call_site()).into());
|
||||
|
||||
let new_name: Ident = Ident::new(&format!("icon_{}", out.len()), Span::call_site());
|
||||
|
||||
segments.push(new_name.clone().into());
|
||||
|
||||
// Replace this instance with a component
|
||||
let mut new_comp = BodyNode::Component(Component {
|
||||
name: syn::Path {
|
||||
leading_colon: None,
|
||||
segments,
|
||||
},
|
||||
prop_gen_args: None,
|
||||
fields: vec![],
|
||||
children: vec![],
|
||||
manual_props: None,
|
||||
});
|
||||
|
||||
std::mem::swap(child, &mut new_comp);
|
||||
|
||||
// And push the original svg into the svg list
|
||||
out.push(new_comp);
|
||||
}
|
||||
|
||||
BodyNode::Element(el) => collect_svgs(&mut el.children, out),
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ifmt_from_text(text: &str) -> IfmtInput {
|
||||
IfmtInput {
|
||||
source: Some(LitStr::new(text, Span::call_site())),
|
||||
segments: vec![],
|
||||
}
|
||||
}
|
||||
+43
-1
@@ -1 +1,43 @@
|
||||
# The actual RSX language implemented using syn parsers.
|
||||
# Dioxus-RSX
|
||||
|
||||
[![Crates.io][crates-badge]][crates-url]
|
||||
[![MIT licensed][mit-badge]][mit-url]
|
||||
[![Build Status][actions-badge]][actions-url]
|
||||
[![Discord chat][discord-badge]][discord-url]
|
||||
|
||||
[crates-badge]: https://img.shields.io/crates/v/dioxus-rsx.svg
|
||||
[crates-url]: https://crates.io/crates/dioxus-rsx
|
||||
|
||||
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||
[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
|
||||
|
||||
[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
|
||||
[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster
|
||||
|
||||
[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square
|
||||
[discord-url]: https://discord.gg/XgGxMSkvUM
|
||||
|
||||
[Website](https://dioxuslabs.com) |
|
||||
[Guides](https://dioxuslabs.com/guide/) |
|
||||
[API Docs](https://docs.rs/dioxus-rsx/latest/dioxus-rsx) |
|
||||
[Chat](https://discord.gg/XgGxMSkvUM)
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
This crate provides the actual DSL that Dioxus uses in the `rsx!` macro. This crate is separate from the macro crate to enable tooling like autoformat, translation, and AST manipulation (extract to component).
|
||||
|
||||
## Contributing
|
||||
|
||||
- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
|
||||
- Join the discord and ask questions!
|
||||
|
||||
## License
|
||||
This project is licensed under the [MIT license].
|
||||
|
||||
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in Dioxus by you, shall be licensed as MIT, without any additional
|
||||
terms or conditions.
|
||||
|
||||
+1
-28
@@ -51,33 +51,6 @@ let _ = dom.rebuild();
|
||||
let content = dioxus_ssr::render_vdom(&dom);
|
||||
```
|
||||
|
||||
## Configuring output
|
||||
It's possible to configure the output of the generated HTML.
|
||||
|
||||
```rust, ignore
|
||||
let content = dioxus_ssr::render_vdom(&dom, |config| config.pretty(true).prerender(true));
|
||||
```
|
||||
|
||||
## Usage as a writer
|
||||
|
||||
We provide the basic `SsrFormatter` object that implements `Display`, so you can integrate SSR into an existing string, or write directly to a file.
|
||||
|
||||
```rust, ignore
|
||||
use std::fmt::{Error, Write};
|
||||
|
||||
let mut buf = String::new();
|
||||
|
||||
let dom = VirtualDom::new(app);
|
||||
let _ = dom.rebuild();
|
||||
|
||||
let args = dioxus_ssr::formatter(dom, |config| config);
|
||||
buf.write_fmt!(format_args!("{}", args));
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -95,7 +68,7 @@ To enable pre-rendering, simply configure the `SsrConfig` with pre-rendering ena
|
||||
```rust, ignore
|
||||
let dom = VirtualDom::new(App);
|
||||
|
||||
let text = dioxus_ssr::render_vdom(App, |cfg| cfg.pre_render(true));
|
||||
let text = dioxus_ssr::render_vdom(App, Config { pre_render: true, ..Default::default() });
|
||||
```
|
||||
|
||||
## Usage in server-side rendering
|
||||
|
||||
+42
-7
@@ -1,11 +1,46 @@
|
||||
# Dioxus-Web
|
||||
# Dioxus-web
|
||||
|
||||
Build interactive user experiences directly in the web browser!
|
||||
[![Crates.io][crates-badge]][crates-url]
|
||||
[![MIT licensed][mit-badge]][mit-url]
|
||||
[![Build Status][actions-badge]][actions-url]
|
||||
[![Discord chat][discord-badge]][discord-url]
|
||||
|
||||
Dioxus-web provides a `WebsysRenderer` for the Dioxus Virtual Dom that handles events, progresses components, and updates the actual DOM using web-sys methods.
|
||||
[crates-badge]: https://img.shields.io/crates/v/rsx-rosetta.svg
|
||||
[crates-url]: https://crates.io/crates/rsx-rosetta
|
||||
|
||||
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||
[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
|
||||
|
||||
[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
|
||||
[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster
|
||||
|
||||
[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square
|
||||
[discord-url]: https://discord.gg/XgGxMSkvUM
|
||||
|
||||
[Website](https://dioxuslabs.com) |
|
||||
[Guides](https://dioxuslabs.com/guide/) |
|
||||
[API Docs](https://docs.rs/rsx-rosetta/latest/rsx-rosetta) |
|
||||
[Chat](https://discord.gg/XgGxMSkvUM)
|
||||
|
||||
## Overview
|
||||
|
||||
Run Dioxus in the browser using WebAssembly.
|
||||
|
||||
- Relies on sledgehammer and websys to modify the dom
|
||||
- Supports instant hotreloading via the Dioxus CLI
|
||||
- Around 60k gzipped
|
||||
|
||||
## Contributing
|
||||
|
||||
- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
|
||||
- Join the discord and ask questions!
|
||||
|
||||
## License
|
||||
This project is licensed under the [MIT license].
|
||||
|
||||
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in Dioxus by you, shall be licensed as MIT, without any additional
|
||||
terms or conditions.
|
||||
|
||||
## Web-specific Optimizations
|
||||
- Uses string interning of all common node types
|
||||
- Optimistically interns short strings
|
||||
- Builds trees completely before mounting them
|
||||
Reference in new issue
Block a user