mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-10 06:34:20 +00:00
Merge branch 'upstream' into simplify-native-core
This commit is contained in:
parent
4b81cdbf1b
commit
41bf87e74f
63 changed files with 818 additions and 245 deletions
4
.github/workflows/docs stable.yml
vendored
4
.github/workflows/docs stable.yml
vendored
|
@ -24,7 +24,9 @@ jobs:
|
|||
- name: Build
|
||||
run: cd docs &&
|
||||
cd guide && mdbook build -d ../nightly/guide && cd .. &&
|
||||
cd router && mdbook build -d ../nightly/router && cd ..
|
||||
cd router && mdbook build -d ../nightly/router && cd ..
|
||||
# cd reference && mdbook build -d ../nightly/reference && cd .. &&
|
||||
# cd fermi && mdbook build -d ../nightly/fermi && cd ..
|
||||
|
||||
- name: Deploy 🚀
|
||||
uses: JamesIves/github-pages-deploy-action@v4.4.1
|
||||
|
|
4
.github/workflows/docs.yml
vendored
4
.github/workflows/docs.yml
vendored
|
@ -29,7 +29,9 @@ jobs:
|
|||
- name: Build
|
||||
run: cd docs &&
|
||||
cd guide && mdbook build -d ../nightly/guide && cd .. &&
|
||||
cd router && mdbook build -d ../nightly/router && cd ..
|
||||
cd router && mdbook build -d ../nightly/router && cd ..
|
||||
# cd reference && mdbook build -d ../nightly/reference && cd .. &&
|
||||
# cd fermi && mdbook build -d ../nightly/fermi && cd ..
|
||||
|
||||
- name: Deploy 🚀
|
||||
uses: JamesIves/github-pages-deploy-action@v4.4.1
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
<span> | </span>
|
||||
<a href="https://github.com/DioxusLabs/example-projects"> Examples </a>
|
||||
<span> | </span>
|
||||
<a href="https://dioxuslabs.com/guide"> Guide </a>
|
||||
<a href="https://dioxuslabs.com/docs/0.3/guide/en/"> Guide </a>
|
||||
<span> | </span>
|
||||
<a href="https://github.com/DioxusLabs/dioxus/blob/master/notes/README/ZH_CN.md"> 中文 </a>
|
||||
<span> | </span>
|
||||
|
|
|
@ -7,7 +7,7 @@ fn main() {}
|
|||
struct AppSettings {}
|
||||
|
||||
// ANCHOR: wrap_context
|
||||
fn use_settings(cx: &ScopeState) -> UseSharedState<AppSettings> {
|
||||
fn use_settings(cx: &ScopeState) -> &UseSharedState<AppSettings> {
|
||||
use_shared_state::<AppSettings>(cx).expect("App settings not provided")
|
||||
}
|
||||
// ANCHOR_END: wrap_context
|
||||
|
|
|
@ -14,7 +14,7 @@ Examples:
|
|||
|
||||
The desktop is a powerful target for Dioxus but is currently limited in capability when compared to the Web platform. Currently, desktop apps are rendered with the platform's WebView library, but your Rust code is running natively on a native thread. This means that browser APIs are *not* available, so rendering WebGL, Canvas, etc is not as easy as the Web. However, native system APIs *are* accessible, so streaming, WebSockets, filesystem, etc are all viable APIs. In the future, we plan to move to a custom web renderer-based DOM renderer with WGPU integrations.
|
||||
|
||||
Dioxus Desktop is built off [Tauri](https://tauri.app/). Right now there aren't any Dioxus abstractions over keyboard shortcuts, menubar, handling, etc, so you'll want to leverage Tauri – mostly [Wry](http://github.com/tauri-apps/wry/) and [Tao](http://github.com/tauri-apps/tao)) directly.
|
||||
Dioxus Desktop is built off [Tauri](https://tauri.app/). Right now there aren't any Dioxus abstractions over the menubar, handling, etc, so you'll want to leverage Tauri – mostly [Wry](http://github.com/tauri-apps/wry/) and [Tao](http://github.com/tauri-apps/tao)) directly.
|
||||
|
||||
# Getting started
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ For example, if many components need to access an `AppSettings` struct, you can
|
|||
|
||||
## Custom Hook Logic
|
||||
|
||||
You can use [`cx.use_hook`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.Scope.html#method.use_hook) to build your own hooks. In fact, this is what all the standard hooks are built on!
|
||||
You can use [`cx.use_hook`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.ScopeState.html#method.use_hook) to build your own hooks. In fact, this is what all the standard hooks are built on!
|
||||
|
||||
`use_hook` accepts a single closure for initializing the hook. It will be only run the first time the component is rendered. The return value of that closure will be used as the value of the hook – Dioxus will take it, and store it for as long as the component is alive. On every render (not just the first one!), you will get a reference to this value.
|
||||
|
||||
|
|
|
@ -63,7 +63,7 @@ This very site is built with Dioxus, and the source code is available [here](htt
|
|||
|
||||
To get started with Dioxus, check out any of the "Getting Started" guides for your platform of choice, or check out the GitHub Repository for more details.
|
||||
|
||||
- [Getting Started with Dioxus](https://dioxuslabs.com/guide)
|
||||
- [Getting Started with Dioxus](https://dioxuslabs.com/guide/en)
|
||||
- [Getting Started with Web](https://dioxuslabs.com/reference/web)
|
||||
- [Getting Started with Desktop](https://dioxuslabs.com/reference/desktop)
|
||||
- [Getting Started with Mobile](https://dioxuslabs.com/reference/mobile)
|
||||
|
@ -163,7 +163,7 @@ Today, to publish a Dioxus app, you don't need NPM/WebPack/Parcel/etc. Dioxus si
|
|||
|
||||
## Show me more
|
||||
|
||||
Here, we'll dive into some features of Dioxus and why it's so fun to use. The [guide](https://dioxuslabs.com/guide/) serves as a deeper and more comprehensive look at what Dioxus can do.
|
||||
Here, we'll dive into some features of Dioxus and why it's so fun to use. The [guide](https://dioxuslabs.com/guide/en/) serves as a deeper and more comprehensive look at what Dioxus can do.
|
||||
|
||||
## Building a new project is simple
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
**Dioxus** é um framework e ecossistema para desenvolver interfaces rápidas, escaláveis e robustas com a linguagem de Programação Rust. Este guia irá ajudar você a começar com o Dioxus para Web, Desktop, Móvel e mais.
|
||||
|
||||
> Este livro é a Referência e Guias Avançados para o framework Dioxus. Para um tutorial em como de fato _usar_ o Dioxus, procure o [guia oficial](https://dioxuslabs.com/guide/).
|
||||
> Este livro é a Referência e Guias Avançados para o framework Dioxus. Para um tutorial em como de fato _usar_ o Dioxus, procure o [guia oficial](https://dioxuslabs.com/guide/en/).
|
||||
|
||||
## Guias e Referência
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
One of Dioxus' killer features is the ability to quickly build a native desktop app that looks and feels the same across platforms. Apps built with Dioxus are typically <5mb in size and use existing system resources, so they won't hog extreme amounts of RAM or memory.
|
||||
|
||||
Dioxus Desktop is built off Tauri. Right now there aren't any Dioxus abstractions over keyboard shortcuts, menubar, handling, etc, so you'll want to leverage Tauri - mostly [Wry](http://github.com/tauri-apps/wry/) and [Tao](http://github.com/tauri-apps/tao)) directly. The next major release of Dioxus-Desktop will include components and hooks for notifications, global shortcuts, menubar, etc.
|
||||
Dioxus Desktop is built off Tauri. Right now there aren't any Dioxus abstractions over the menubar, handling, etc, so you'll want to leverage Tauri - mostly [Wry](http://github.com/tauri-apps/wry/) and [Tao](http://github.com/tauri-apps/tao)) directly. The next major release of Dioxus-Desktop will include components and hooks for notifications, global shortcuts, menubar, etc.
|
||||
|
||||
## Getting Set up
|
||||
|
||||
|
@ -42,4 +42,4 @@ 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!
|
||||
Make sure to read the [Dioxus Guide](https://dioxuslabs.com/guide/en) if you already haven't!
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
# Dioxus Router: Introduction
|
||||
|
||||
Whether or not you're building a website, desktop app, or mobile app, organizing your app's views into "pages" can be an effective method for organization and maintainability.
|
||||
|
||||
Dioxus comes with a router built-in. To start utilizing Dioxus Router, enable the ``router`` feature in your ``Cargo.toml`` file.
|
||||
```toml
|
||||
[dependencies]
|
||||
dioxus = { version = "x.x.x", features = [.., "router"] }
|
||||
```
|
||||
The `dioxus-router` crate contains the Router module. To add it to your project run:
|
||||
|
||||
cargo add dioxus-router
|
||||
|
||||
> **Be sure to include the `web` feature (`--feature web`) for deployment into a browser!**
|
||||
|
||||
In this book you'll find a short [guide](./guide/index.md) to get up to speed with Dioxus Router, as well as the router's [reference](./reference/index.md).
|
17
examples/shortcut.rs
Normal file
17
examples/shortcut.rs
Normal file
|
@ -0,0 +1,17 @@
|
|||
use dioxus::prelude::*;
|
||||
use dioxus_desktop::tao::keyboard::ModifiersState;
|
||||
use dioxus_desktop::use_global_shortcut;
|
||||
|
||||
fn main() {
|
||||
dioxus_desktop::launch(app);
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
let toggled = use_state(cx, || false);
|
||||
use_global_shortcut(cx, KeyCode::S, ModifiersState::CONTROL, {
|
||||
to_owned![toggled];
|
||||
move || toggled.modify(|t| !*t)
|
||||
});
|
||||
|
||||
cx.render(rsx!("toggle: {toggled.get()}"))
|
||||
}
|
|
@ -40,7 +40,7 @@
|
|||
<span> | </span>
|
||||
<a href="https://github.com/DioxusLabs/example-projects"> 代码示例 </a>
|
||||
<span> | </span>
|
||||
<a href="https://dioxuslabs.com/guide"> 开发指南 </a>
|
||||
<a href="https://dioxuslabs.com/guide/en"> 开发指南 </a>
|
||||
<span> | </span>
|
||||
<a href="https://github.com/DioxusLabs/dioxus/blob/master/README.md"> English </a>
|
||||
<span> | </span>
|
||||
|
|
|
@ -6,7 +6,7 @@ edition = "2021"
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dioxus-rsx = { path = "../rsx", version = "0.0.2" }
|
||||
dioxus-rsx = { path = "../rsx", version = "^0.0.3" }
|
||||
proc-macro2 = { version = "1.0.6", features = ["span-locations"] }
|
||||
quote = "1.0"
|
||||
syn = { version = "1.0.11", features = ["full", "extra-traits"] }
|
||||
|
|
|
@ -18,7 +18,7 @@ proc-macro = true
|
|||
proc-macro2 = { version = "1.0" }
|
||||
quote = "1.0"
|
||||
syn = { version = "1.0", features = ["full", "extra-traits"] }
|
||||
dioxus-rsx = { path = "../rsx", version = "0.0.2" }
|
||||
dioxus-rsx = { path = "../rsx", version = "^0.0.3" }
|
||||
|
||||
# testing
|
||||
[dev-dependencies]
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
[discord-url]: https://discord.gg/XgGxMSkvUM
|
||||
|
||||
[Website](https://dioxuslabs.com) |
|
||||
[Guides](https://dioxuslabs.com/guide/) |
|
||||
[Guides](https://dioxuslabs.com/docs/0.3/guide/en/) |
|
||||
[API Docs](https://docs.rs/dioxus-core-macro/latest/dioxus_core_macro) |
|
||||
[Chat](https://discord.gg/XgGxMSkvUM)
|
||||
|
||||
|
|
|
@ -145,6 +145,25 @@ impl ToTokens for InlinePropsBody {
|
|||
(quote! { #lifetime, }, fn_generics, quote! { #generics })
|
||||
};
|
||||
|
||||
let generics_no_bounds = {
|
||||
let mut generics = generics.clone();
|
||||
generics.params = generics
|
||||
.params
|
||||
.iter()
|
||||
.map(|it| match it {
|
||||
GenericParam::Type(tp) => {
|
||||
let mut tp = tp.clone();
|
||||
tp.bounds.clear();
|
||||
|
||||
GenericParam::Type(tp)
|
||||
}
|
||||
_ => it.clone(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
generics
|
||||
};
|
||||
|
||||
out_tokens.append_all(quote! {
|
||||
#modifiers
|
||||
#[allow(non_camel_case_types)]
|
||||
|
@ -155,7 +174,7 @@ impl ToTokens for InlinePropsBody {
|
|||
}
|
||||
|
||||
#(#attrs)*
|
||||
#maybe_async #vis fn #ident #fn_generics (#cx_token: Scope<#scope_lifetime #struct_name #generics>) #output
|
||||
#maybe_async #vis fn #ident #fn_generics (#cx_token: Scope<#scope_lifetime #struct_name #generics_no_bounds>) #output
|
||||
#where_clause
|
||||
{
|
||||
let #struct_name { #(#field_names),* } = &#cx_token.props;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "dioxus-core"
|
||||
version = "0.3.1"
|
||||
version = "0.3.2"
|
||||
authors = ["Jonathan Kelley"]
|
||||
edition = "2018"
|
||||
description = "Core functionality for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences"
|
||||
|
|
|
@ -175,10 +175,10 @@ impl VirtualDom {
|
|||
}
|
||||
|
||||
impl ElementPath {
|
||||
pub(crate) fn is_ascendant(&self, big: &&[u8]) -> bool {
|
||||
pub(crate) fn is_decendant(&self, small: &&[u8]) -> bool {
|
||||
match *self {
|
||||
ElementPath::Deep(small) => small.len() <= big.len() && small == &big[..small.len()],
|
||||
ElementPath::Root(r) => big.len() == 1 && big[0] == r as u8,
|
||||
ElementPath::Deep(big) => small.len() <= big.len() && *small == &big[..small.len()],
|
||||
ElementPath::Root(r) => small.len() == 1 && small[0] == r as u8,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,8 +25,8 @@ fn sort_bfs(paths: &[&'static [u8]]) -> Vec<(usize, &'static [u8])> {
|
|||
}
|
||||
}
|
||||
// The shorter path goes first
|
||||
(Some(_), None) => return std::cmp::Ordering::Less,
|
||||
(None, Some(_)) => return std::cmp::Ordering::Greater,
|
||||
(None, Some(_)) => return std::cmp::Ordering::Less,
|
||||
(Some(_), None) => return std::cmp::Ordering::Greater,
|
||||
(None, None) => return std::cmp::Ordering::Equal,
|
||||
}
|
||||
}
|
||||
|
@ -40,14 +40,25 @@ fn sorting() {
|
|||
(0, &[0, 1]),
|
||||
(1, &[0, 2]),
|
||||
(2, &[1, 0]),
|
||||
(4, &[1, 1]),
|
||||
(3, &[1, 2]),
|
||||
(3, &[1, 0, 1]),
|
||||
(4, &[1, 2]),
|
||||
];
|
||||
assert_eq!(
|
||||
sort_bfs(&[&[0, 1,], &[0, 2,], &[1, 0,], &[1, 2,], &[1, 1,],]),
|
||||
sort_bfs(&[&[0, 1,], &[0, 2,], &[1, 0,], &[1, 0, 1,], &[1, 2,],]),
|
||||
r
|
||||
);
|
||||
let r: [(usize, &[u8]); 6] = [
|
||||
(0, &[0]),
|
||||
(1, &[0, 1]),
|
||||
(2, &[0, 1, 2]),
|
||||
(3, &[1]),
|
||||
(4, &[1, 2]),
|
||||
(5, &[2]),
|
||||
];
|
||||
assert_eq!(
|
||||
sort_bfs(&[&[0], &[0, 1], &[0, 1, 2], &[1], &[1, 2], &[2],]),
|
||||
r
|
||||
);
|
||||
assert!(matches!(&[0], &[_, ..]))
|
||||
}
|
||||
|
||||
impl<'b> VirtualDom {
|
||||
|
|
|
@ -717,7 +717,7 @@ impl<'b> VirtualDom {
|
|||
Fragment(nodes) => nodes
|
||||
.iter()
|
||||
.map(|node| self.push_all_real_nodes(node))
|
||||
.count(),
|
||||
.sum(),
|
||||
|
||||
Component(comp) => {
|
||||
let scope = comp.scope.get().unwrap();
|
||||
|
@ -729,7 +729,7 @@ impl<'b> VirtualDom {
|
|||
}
|
||||
}
|
||||
})
|
||||
.count()
|
||||
.sum()
|
||||
}
|
||||
|
||||
fn create_children(&mut self, nodes: impl IntoIterator<Item = &'b VNode<'b>>) -> usize {
|
||||
|
|
|
@ -72,9 +72,9 @@ pub struct LocalTaskHandle {
|
|||
|
||||
impl ArcWake for LocalTaskHandle {
|
||||
fn wake_by_ref(arc_self: &Arc<Self>) {
|
||||
arc_self
|
||||
// This can fail if the scheduler has been dropped while the application is shutting down
|
||||
let _ = arc_self
|
||||
.tx
|
||||
.unbounded_send(SchedulerMsg::TaskNotified(arc_self.id))
|
||||
.unwrap();
|
||||
.unbounded_send(SchedulerMsg::TaskNotified(arc_self.id));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -373,17 +373,12 @@ impl<'src> ScopeState {
|
|||
None
|
||||
}
|
||||
|
||||
/// Expose state to children further down the [`crate::VirtualDom`] Tree. Does not require `clone` on the context,
|
||||
/// though we do recommend it.
|
||||
/// Expose state to children further down the [`crate::VirtualDom`] Tree. Requires `Clone` on the context to allow getting values down the tree.
|
||||
///
|
||||
/// This is a "fundamental" operation and should only be called during initialization of a hook.
|
||||
///
|
||||
/// For a hook that provides the same functionality, use `use_provide_context` and `use_context` instead.
|
||||
///
|
||||
/// If a state is provided that already exists, the new value will not be inserted. Instead, this method will
|
||||
/// return the existing value. This behavior is chosen so shared values do not need to be `Clone`. This particular
|
||||
/// behavior might change in the future.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust, ignore
|
||||
|
|
|
@ -396,7 +396,7 @@ impl VirtualDom {
|
|||
|
||||
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
|
||||
if attr.name.trim_start_matches("on") == name
|
||||
&& target_path.is_ascendant(&this_path)
|
||||
&& target_path.is_decendant(&this_path)
|
||||
{
|
||||
listeners.push(&attr.value);
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
[discord-url]: https://discord.gg/XgGxMSkvUM
|
||||
|
||||
[Website](https://dioxuslabs.com) |
|
||||
[Guides](https://dioxuslabs.com/guide/) |
|
||||
[Guides](https://dioxuslabs.com/docs/0.3/guide/en/) |
|
||||
[API Docs](https://docs.rs/dioxus-desktop/latest/dioxus_desktop) |
|
||||
[Chat](https://discord.gg/XgGxMSkvUM)
|
||||
|
||||
|
@ -30,7 +30,7 @@
|
|||
|
||||
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
|
||||
[guide-url]: https://dioxuslabs.com/docs/0.3/guide/en/getting_started/desktop.html
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -5,6 +5,11 @@ use std::rc::Weak;
|
|||
use crate::create_new_window;
|
||||
use crate::eval::EvalResult;
|
||||
use crate::events::IpcMessage;
|
||||
use crate::shortcut::IntoKeyCode;
|
||||
use crate::shortcut::IntoModifersState;
|
||||
use crate::shortcut::ShortcutId;
|
||||
use crate::shortcut::ShortcutRegistry;
|
||||
use crate::shortcut::ShortcutRegistryError;
|
||||
use crate::Config;
|
||||
use crate::WebviewHandler;
|
||||
use dioxus_core::ScopeState;
|
||||
|
@ -63,6 +68,8 @@ pub struct DesktopContext {
|
|||
|
||||
pub(crate) event_handlers: WindowEventHandlers,
|
||||
|
||||
pub(crate) shortcut_manager: ShortcutRegistry,
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
pub(crate) views: Rc<RefCell<Vec<*mut objc::runtime::Object>>>,
|
||||
}
|
||||
|
@ -83,6 +90,7 @@ impl DesktopContext {
|
|||
event_loop: EventLoopWindowTarget<UserWindowEvent>,
|
||||
webviews: WebviewQueue,
|
||||
event_handlers: WindowEventHandlers,
|
||||
shortcut_manager: ShortcutRegistry,
|
||||
) -> Self {
|
||||
Self {
|
||||
webview,
|
||||
|
@ -91,6 +99,7 @@ impl DesktopContext {
|
|||
eval: tokio::sync::broadcast::channel(8).0,
|
||||
pending_windows: webviews,
|
||||
event_handlers,
|
||||
shortcut_manager,
|
||||
#[cfg(target_os = "ios")]
|
||||
views: Default::default(),
|
||||
}
|
||||
|
@ -111,6 +120,7 @@ impl DesktopContext {
|
|||
dom,
|
||||
&self.pending_windows,
|
||||
&self.event_handlers,
|
||||
self.shortcut_manager.clone(),
|
||||
);
|
||||
|
||||
let id = window.webview.window().id();
|
||||
|
@ -240,6 +250,32 @@ impl DesktopContext {
|
|||
self.event_handlers.remove(id)
|
||||
}
|
||||
|
||||
/// Create a global shortcut
|
||||
///
|
||||
/// Linux: Only works on x11. See [this issue](https://github.com/tauri-apps/tao/issues/331) for more information.
|
||||
pub fn create_shortcut(
|
||||
&self,
|
||||
key: impl IntoKeyCode,
|
||||
modifiers: impl IntoModifersState,
|
||||
callback: impl FnMut() + 'static,
|
||||
) -> Result<ShortcutId, ShortcutRegistryError> {
|
||||
self.shortcut_manager.add_shortcut(
|
||||
modifiers.into_modifiers_state(),
|
||||
key.into_key_code(),
|
||||
Box::new(callback),
|
||||
)
|
||||
}
|
||||
|
||||
/// Remove a global shortcut
|
||||
pub fn remove_shortcut(&self, id: ShortcutId) {
|
||||
self.shortcut_manager.remove_shortcut(id)
|
||||
}
|
||||
|
||||
/// Remove all global shortcuts
|
||||
pub fn remove_all_shortcuts(&self) {
|
||||
self.shortcut_manager.remove_all()
|
||||
}
|
||||
|
||||
/// Push an objc view to the window
|
||||
#[cfg(target_os = "ios")]
|
||||
pub fn push_view(&self, view: objc_id::ShareId<objc::runtime::Object>) {
|
||||
|
|
|
@ -9,6 +9,7 @@ mod escape;
|
|||
mod eval;
|
||||
mod events;
|
||||
mod protocol;
|
||||
mod shortcut;
|
||||
mod waker;
|
||||
mod webview;
|
||||
|
||||
|
@ -21,6 +22,8 @@ use dioxus_core::*;
|
|||
use dioxus_html::HtmlEvent;
|
||||
pub use eval::{use_eval, EvalResult};
|
||||
use futures_util::{pin_mut, FutureExt};
|
||||
use shortcut::ShortcutRegistry;
|
||||
pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError};
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
use std::task::Waker;
|
||||
|
@ -139,6 +142,8 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
|
|||
|
||||
let queue = WebviewQueue::default();
|
||||
|
||||
let shortcut_manager = ShortcutRegistry::new(&event_loop);
|
||||
|
||||
// By default, we'll create a new window when the app starts
|
||||
queue.borrow_mut().push(create_new_window(
|
||||
cfg,
|
||||
|
@ -147,6 +152,7 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
|
|||
VirtualDom::new_with_props(root, props),
|
||||
&queue,
|
||||
&event_handlers,
|
||||
shortcut_manager.clone(),
|
||||
));
|
||||
|
||||
event_loop.run(move |window_event, event_loop, control_flow| {
|
||||
|
@ -260,6 +266,7 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
|
|||
|
||||
_ => {}
|
||||
},
|
||||
Event::GlobalShortcutEvent(id) => shortcut_manager.call_handlers(id),
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
|
@ -272,6 +279,7 @@ fn create_new_window(
|
|||
dom: VirtualDom,
|
||||
queue: &WebviewQueue,
|
||||
event_handlers: &WindowEventHandlers,
|
||||
shortcut_manager: ShortcutRegistry,
|
||||
) -> WebviewHandler {
|
||||
let webview = webview::build(&mut cfg, event_loop, proxy.clone());
|
||||
|
||||
|
@ -281,6 +289,7 @@ fn create_new_window(
|
|||
event_loop.clone(),
|
||||
queue.clone(),
|
||||
event_handlers.clone(),
|
||||
shortcut_manager,
|
||||
));
|
||||
|
||||
let id = webview.window().id();
|
||||
|
|
|
@ -6,7 +6,7 @@ Render the Dioxus VirtualDom using the platform's native WebView implementation.
|
|||
|
||||
One of Dioxus' flagship features is the ability to quickly build a native desktop app that looks and feels the same across platforms. Apps built with Dioxus are typically <5mb in size and use existing system resources, so they won't hog extreme amounts of RAM or memory.
|
||||
|
||||
Dioxus Desktop is built off Tauri. Right now there aren't any Dioxus abstractions over keyboard shortcuts, menubar, handling, etc, so you'll want to leverage Tauri - mostly [Wry](http://github.com/tauri-apps/wry/) and [Tao](http://github.com/tauri-apps/tao) directly. An upcoming release of Dioxus-Desktop will include components and hooks for notifications, global shortcuts, menubar, etc.
|
||||
Dioxus Desktop is built off Tauri. Right now there aren't any Dioxus abstractions over the menubar, handling, etc, so you'll want to leverage Tauri - mostly [Wry](http://github.com/tauri-apps/wry/) and [Tao](http://github.com/tauri-apps/tao) directly. An upcoming release of Dioxus-Desktop will include components and hooks for notifications, global shortcuts, menubar, etc.
|
||||
|
||||
|
||||
## Getting Set up
|
||||
|
@ -49,4 +49,4 @@ 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!
|
||||
Make sure to read the [Dioxus Guide](https://dioxuslabs.com/docs/0.3/guide/en) if you already haven't!
|
||||
|
|
332
packages/desktop/src/shortcut.rs
Normal file
332
packages/desktop/src/shortcut.rs
Normal file
|
@ -0,0 +1,332 @@
|
|||
use std::{cell::RefCell, collections::HashMap, rc::Rc};
|
||||
|
||||
use dioxus_core::ScopeState;
|
||||
use dioxus_html::input_data::keyboard_types::Modifiers;
|
||||
use slab::Slab;
|
||||
use wry::application::{
|
||||
accelerator::{Accelerator, AcceleratorId},
|
||||
event_loop::EventLoopWindowTarget,
|
||||
global_shortcut::{GlobalShortcut, ShortcutManager, ShortcutManagerError},
|
||||
keyboard::{KeyCode, ModifiersState},
|
||||
};
|
||||
|
||||
use crate::{use_window, DesktopContext};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ShortcutRegistry {
|
||||
manager: Rc<RefCell<ShortcutManager>>,
|
||||
shortcuts: ShortcutMap,
|
||||
}
|
||||
|
||||
type ShortcutMap = Rc<RefCell<HashMap<AcceleratorId, Shortcut>>>;
|
||||
|
||||
struct Shortcut {
|
||||
shortcut: GlobalShortcut,
|
||||
callbacks: Slab<Box<dyn FnMut()>>,
|
||||
}
|
||||
|
||||
impl Shortcut {
|
||||
fn insert(&mut self, callback: Box<dyn FnMut()>) -> usize {
|
||||
self.callbacks.insert(callback)
|
||||
}
|
||||
|
||||
fn remove(&mut self, id: usize) {
|
||||
let _ = self.callbacks.remove(id);
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.callbacks.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl ShortcutRegistry {
|
||||
pub fn new<T>(target: &EventLoopWindowTarget<T>) -> Self {
|
||||
let myself = Self {
|
||||
manager: Rc::new(RefCell::new(ShortcutManager::new(target))),
|
||||
shortcuts: Rc::new(RefCell::new(HashMap::new())),
|
||||
};
|
||||
// prevent CTRL+R from reloading the page which breaks apps
|
||||
let _ = myself.add_shortcut(
|
||||
Some(ModifiersState::CONTROL),
|
||||
KeyCode::KeyR,
|
||||
Box::new(|| {}),
|
||||
);
|
||||
myself
|
||||
}
|
||||
|
||||
pub(crate) fn call_handlers(&self, id: AcceleratorId) {
|
||||
if let Some(Shortcut { callbacks, .. }) = self.shortcuts.borrow_mut().get_mut(&id) {
|
||||
for (_, callback) in callbacks.iter_mut() {
|
||||
(callback)();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn add_shortcut(
|
||||
&self,
|
||||
modifiers: impl Into<Option<ModifiersState>>,
|
||||
key: KeyCode,
|
||||
callback: Box<dyn FnMut()>,
|
||||
) -> Result<ShortcutId, ShortcutRegistryError> {
|
||||
let accelerator = Accelerator::new(modifiers, key);
|
||||
let accelerator_id = accelerator.clone().id();
|
||||
let mut shortcuts = self.shortcuts.borrow_mut();
|
||||
Ok(
|
||||
if let Some(callbacks) = shortcuts.get_mut(&accelerator_id) {
|
||||
let id = callbacks.insert(callback);
|
||||
ShortcutId {
|
||||
id: accelerator_id,
|
||||
number: id,
|
||||
}
|
||||
} else {
|
||||
match self.manager.borrow_mut().register(accelerator) {
|
||||
Ok(global_shortcut) => {
|
||||
let mut slab = Slab::new();
|
||||
let id = slab.insert(callback);
|
||||
let shortcut = Shortcut {
|
||||
shortcut: global_shortcut,
|
||||
callbacks: slab,
|
||||
};
|
||||
shortcuts.insert(accelerator_id, shortcut);
|
||||
ShortcutId {
|
||||
id: accelerator_id,
|
||||
number: id,
|
||||
}
|
||||
}
|
||||
Err(ShortcutManagerError::InvalidAccelerator(shortcut)) => {
|
||||
return Err(ShortcutRegistryError::InvalidShortcut(shortcut))
|
||||
}
|
||||
Err(err) => return Err(ShortcutRegistryError::Other(Box::new(err))),
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn remove_shortcut(&self, id: ShortcutId) {
|
||||
let mut shortcuts = self.shortcuts.borrow_mut();
|
||||
if let Some(callbacks) = shortcuts.get_mut(&id.id) {
|
||||
callbacks.remove(id.number);
|
||||
if callbacks.is_empty() {
|
||||
if let Some(shortcut) = shortcuts.remove(&id.id) {
|
||||
let _ = self.manager.borrow_mut().unregister(shortcut.shortcut);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn remove_all(&self) {
|
||||
let mut shortcuts = self.shortcuts.borrow_mut();
|
||||
shortcuts.clear();
|
||||
let _ = self.manager.borrow_mut().unregister_all();
|
||||
// prevent CTRL+R from reloading the page which breaks apps
|
||||
let _ = self.add_shortcut(
|
||||
Some(ModifiersState::CONTROL),
|
||||
KeyCode::KeyR,
|
||||
Box::new(|| {}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug)]
|
||||
/// An error that can occur when registering a shortcut.
|
||||
pub enum ShortcutRegistryError {
|
||||
/// The shortcut is invalid.
|
||||
InvalidShortcut(String),
|
||||
/// An unknown error occurred.
|
||||
Other(Box<dyn std::error::Error>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
/// An global id for a shortcut.
|
||||
pub struct ShortcutId {
|
||||
id: AcceleratorId,
|
||||
number: usize,
|
||||
}
|
||||
|
||||
/// A global shortcut. This will be automatically removed when it is dropped.
|
||||
pub struct ShortcutHandle {
|
||||
desktop: DesktopContext,
|
||||
/// The id of the shortcut
|
||||
pub shortcut_id: ShortcutId,
|
||||
}
|
||||
|
||||
/// Get a closure that executes any JavaScript in the WebView context.
|
||||
pub fn use_global_shortcut(
|
||||
cx: &ScopeState,
|
||||
key: impl IntoKeyCode,
|
||||
modifiers: impl IntoModifersState,
|
||||
handler: impl FnMut() + 'static,
|
||||
) -> &Result<ShortcutHandle, ShortcutRegistryError> {
|
||||
let desktop = use_window(cx);
|
||||
cx.use_hook(move || {
|
||||
let desktop = desktop.clone();
|
||||
|
||||
let id = desktop.create_shortcut(
|
||||
key.into_key_code(),
|
||||
modifiers.into_modifiers_state(),
|
||||
handler,
|
||||
);
|
||||
|
||||
Ok(ShortcutHandle {
|
||||
desktop,
|
||||
shortcut_id: id?,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
impl ShortcutHandle {
|
||||
/// Remove the shortcut.
|
||||
pub fn remove(&self) {
|
||||
self.desktop.remove_shortcut(self.shortcut_id);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ShortcutHandle {
|
||||
fn drop(&mut self) {
|
||||
self.remove()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IntoModifersState {
|
||||
fn into_modifiers_state(self) -> ModifiersState;
|
||||
}
|
||||
|
||||
impl IntoModifersState for ModifiersState {
|
||||
fn into_modifiers_state(self) -> ModifiersState {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoModifersState for Modifiers {
|
||||
fn into_modifiers_state(self) -> ModifiersState {
|
||||
let mut state = ModifiersState::empty();
|
||||
if self.contains(Modifiers::SHIFT) {
|
||||
state |= ModifiersState::SHIFT
|
||||
}
|
||||
if self.contains(Modifiers::CONTROL) {
|
||||
state |= ModifiersState::CONTROL
|
||||
}
|
||||
if self.contains(Modifiers::ALT) {
|
||||
state |= ModifiersState::ALT
|
||||
}
|
||||
if self.contains(Modifiers::META) || self.contains(Modifiers::SUPER) {
|
||||
state |= ModifiersState::SUPER
|
||||
}
|
||||
state
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IntoKeyCode {
|
||||
fn into_key_code(self) -> KeyCode;
|
||||
}
|
||||
|
||||
impl IntoKeyCode for KeyCode {
|
||||
fn into_key_code(self) -> KeyCode {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoKeyCode for dioxus_html::KeyCode {
|
||||
fn into_key_code(self) -> KeyCode {
|
||||
match self {
|
||||
dioxus_html::KeyCode::Backspace => KeyCode::Backspace,
|
||||
dioxus_html::KeyCode::Tab => KeyCode::Tab,
|
||||
dioxus_html::KeyCode::Clear => KeyCode::NumpadClear,
|
||||
dioxus_html::KeyCode::Enter => KeyCode::Enter,
|
||||
dioxus_html::KeyCode::Shift => KeyCode::ShiftLeft,
|
||||
dioxus_html::KeyCode::Ctrl => KeyCode::ControlLeft,
|
||||
dioxus_html::KeyCode::Alt => KeyCode::AltLeft,
|
||||
dioxus_html::KeyCode::Pause => KeyCode::Pause,
|
||||
dioxus_html::KeyCode::CapsLock => KeyCode::CapsLock,
|
||||
dioxus_html::KeyCode::Escape => KeyCode::Escape,
|
||||
dioxus_html::KeyCode::Space => KeyCode::Space,
|
||||
dioxus_html::KeyCode::PageUp => KeyCode::PageUp,
|
||||
dioxus_html::KeyCode::PageDown => KeyCode::PageDown,
|
||||
dioxus_html::KeyCode::End => KeyCode::End,
|
||||
dioxus_html::KeyCode::Home => KeyCode::Home,
|
||||
dioxus_html::KeyCode::LeftArrow => KeyCode::ArrowLeft,
|
||||
dioxus_html::KeyCode::UpArrow => KeyCode::ArrowUp,
|
||||
dioxus_html::KeyCode::RightArrow => KeyCode::ArrowRight,
|
||||
dioxus_html::KeyCode::DownArrow => KeyCode::ArrowDown,
|
||||
dioxus_html::KeyCode::Insert => KeyCode::Insert,
|
||||
dioxus_html::KeyCode::Delete => KeyCode::Delete,
|
||||
dioxus_html::KeyCode::Num0 => KeyCode::Numpad0,
|
||||
dioxus_html::KeyCode::Num1 => KeyCode::Numpad1,
|
||||
dioxus_html::KeyCode::Num2 => KeyCode::Numpad2,
|
||||
dioxus_html::KeyCode::Num3 => KeyCode::Numpad3,
|
||||
dioxus_html::KeyCode::Num4 => KeyCode::Numpad4,
|
||||
dioxus_html::KeyCode::Num5 => KeyCode::Numpad5,
|
||||
dioxus_html::KeyCode::Num6 => KeyCode::Numpad6,
|
||||
dioxus_html::KeyCode::Num7 => KeyCode::Numpad7,
|
||||
dioxus_html::KeyCode::Num8 => KeyCode::Numpad8,
|
||||
dioxus_html::KeyCode::Num9 => KeyCode::Numpad9,
|
||||
dioxus_html::KeyCode::A => KeyCode::KeyA,
|
||||
dioxus_html::KeyCode::B => KeyCode::KeyB,
|
||||
dioxus_html::KeyCode::C => KeyCode::KeyC,
|
||||
dioxus_html::KeyCode::D => KeyCode::KeyD,
|
||||
dioxus_html::KeyCode::E => KeyCode::KeyE,
|
||||
dioxus_html::KeyCode::F => KeyCode::KeyF,
|
||||
dioxus_html::KeyCode::G => KeyCode::KeyG,
|
||||
dioxus_html::KeyCode::H => KeyCode::KeyH,
|
||||
dioxus_html::KeyCode::I => KeyCode::KeyI,
|
||||
dioxus_html::KeyCode::J => KeyCode::KeyJ,
|
||||
dioxus_html::KeyCode::K => KeyCode::KeyK,
|
||||
dioxus_html::KeyCode::L => KeyCode::KeyL,
|
||||
dioxus_html::KeyCode::M => KeyCode::KeyM,
|
||||
dioxus_html::KeyCode::N => KeyCode::KeyN,
|
||||
dioxus_html::KeyCode::O => KeyCode::KeyO,
|
||||
dioxus_html::KeyCode::P => KeyCode::KeyP,
|
||||
dioxus_html::KeyCode::Q => KeyCode::KeyQ,
|
||||
dioxus_html::KeyCode::R => KeyCode::KeyR,
|
||||
dioxus_html::KeyCode::S => KeyCode::KeyS,
|
||||
dioxus_html::KeyCode::T => KeyCode::KeyT,
|
||||
dioxus_html::KeyCode::U => KeyCode::KeyU,
|
||||
dioxus_html::KeyCode::V => KeyCode::KeyV,
|
||||
dioxus_html::KeyCode::W => KeyCode::KeyW,
|
||||
dioxus_html::KeyCode::X => KeyCode::KeyX,
|
||||
dioxus_html::KeyCode::Y => KeyCode::KeyY,
|
||||
dioxus_html::KeyCode::Z => KeyCode::KeyZ,
|
||||
dioxus_html::KeyCode::Numpad0 => KeyCode::Numpad0,
|
||||
dioxus_html::KeyCode::Numpad1 => KeyCode::Numpad1,
|
||||
dioxus_html::KeyCode::Numpad2 => KeyCode::Numpad2,
|
||||
dioxus_html::KeyCode::Numpad3 => KeyCode::Numpad3,
|
||||
dioxus_html::KeyCode::Numpad4 => KeyCode::Numpad4,
|
||||
dioxus_html::KeyCode::Numpad5 => KeyCode::Numpad5,
|
||||
dioxus_html::KeyCode::Numpad6 => KeyCode::Numpad6,
|
||||
dioxus_html::KeyCode::Numpad7 => KeyCode::Numpad7,
|
||||
dioxus_html::KeyCode::Numpad8 => KeyCode::Numpad8,
|
||||
dioxus_html::KeyCode::Numpad9 => KeyCode::Numpad9,
|
||||
dioxus_html::KeyCode::Multiply => KeyCode::NumpadMultiply,
|
||||
dioxus_html::KeyCode::Add => KeyCode::NumpadAdd,
|
||||
dioxus_html::KeyCode::Subtract => KeyCode::NumpadSubtract,
|
||||
dioxus_html::KeyCode::DecimalPoint => KeyCode::NumpadDecimal,
|
||||
dioxus_html::KeyCode::Divide => KeyCode::NumpadDivide,
|
||||
dioxus_html::KeyCode::F1 => KeyCode::F1,
|
||||
dioxus_html::KeyCode::F2 => KeyCode::F2,
|
||||
dioxus_html::KeyCode::F3 => KeyCode::F3,
|
||||
dioxus_html::KeyCode::F4 => KeyCode::F4,
|
||||
dioxus_html::KeyCode::F5 => KeyCode::F5,
|
||||
dioxus_html::KeyCode::F6 => KeyCode::F6,
|
||||
dioxus_html::KeyCode::F7 => KeyCode::F7,
|
||||
dioxus_html::KeyCode::F8 => KeyCode::F8,
|
||||
dioxus_html::KeyCode::F9 => KeyCode::F9,
|
||||
dioxus_html::KeyCode::F10 => KeyCode::F10,
|
||||
dioxus_html::KeyCode::F11 => KeyCode::F11,
|
||||
dioxus_html::KeyCode::F12 => KeyCode::F12,
|
||||
dioxus_html::KeyCode::NumLock => KeyCode::NumLock,
|
||||
dioxus_html::KeyCode::ScrollLock => KeyCode::ScrollLock,
|
||||
dioxus_html::KeyCode::Semicolon => KeyCode::Semicolon,
|
||||
dioxus_html::KeyCode::EqualSign => KeyCode::Equal,
|
||||
dioxus_html::KeyCode::Comma => KeyCode::Comma,
|
||||
dioxus_html::KeyCode::Period => KeyCode::Period,
|
||||
dioxus_html::KeyCode::ForwardSlash => KeyCode::Slash,
|
||||
dioxus_html::KeyCode::GraveAccent => KeyCode::Backquote,
|
||||
dioxus_html::KeyCode::OpenBracket => KeyCode::BracketLeft,
|
||||
dioxus_html::KeyCode::BackSlash => KeyCode::Backslash,
|
||||
dioxus_html::KeyCode::CloseBraket => KeyCode::BracketRight,
|
||||
dioxus_html::KeyCode::SingleQuote => KeyCode::Quote,
|
||||
key => panic!("Failed to convert {:?} to tao::keyboard::KeyCode, try using tao::keyboard::KeyCode directly", key),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "dioxus"
|
||||
version = "0.3.1"
|
||||
version = "0.3.2"
|
||||
authors = ["Jonathan Kelley", "Dioxus Labs", "ealmloff"]
|
||||
edition = "2021"
|
||||
description = "Core functionality for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences"
|
||||
|
@ -16,7 +16,7 @@ dioxus-core = { path = "../core", version = "^0.3.0" }
|
|||
dioxus-html = { path = "../html", version = "^0.3.0", optional = true }
|
||||
dioxus-core-macro = { path = "../core-macro", version = "^0.3.0", optional = true }
|
||||
dioxus-hooks = { path = "../hooks", version = "^0.3.0", optional = true }
|
||||
dioxus-rsx = { path = "../rsx", version = "0.0.2", optional = true }
|
||||
dioxus-rsx = { path = "../rsx", version = "^0.0.3", optional = true }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
dioxus-hot-reload = { path = "../hot-reload", version = "0.1.0", optional = true }
|
||||
|
|
|
@ -9,9 +9,8 @@
|
|||
|
||||
This overview provides a brief introduction to Dioxus. For a more in-depth guide, make sure to check out:
|
||||
|
||||
- [Getting Started](https://dioxuslabs.com/guide/setup.html)
|
||||
- [Book](https://dioxuslabs.com/guide/)
|
||||
- [Reference](https://dioxuslabs.com/reference)
|
||||
- [Getting Started](https://dioxuslabs.com/docs/0.3/guide/en/getting_started/index.html)
|
||||
- [Book](https://dioxuslabs.com/docs/0.3/guide/en/)
|
||||
- [Examples](https://github.com/DioxusLabs/example-projects)
|
||||
|
||||
# Overview and Goals
|
||||
|
|
|
@ -13,8 +13,8 @@ use crate::Readable;
|
|||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct AtomId {
|
||||
pub(crate) ptr: *const (),
|
||||
pub(crate) type_id: TypeId,
|
||||
pub ptr: *const (),
|
||||
pub type_id: TypeId,
|
||||
}
|
||||
|
||||
pub struct AtomRoot {
|
||||
|
|
|
@ -20,3 +20,4 @@ log = "0.4"
|
|||
[dev-dependencies]
|
||||
futures-util = { version = "0.3", default-features = false }
|
||||
dioxus-core = { path = "../../packages/core", version = "^0.3.0" }
|
||||
dioxus = { path = "../../packages/dioxus", version = "^0.3.0" }
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
[discord-url]: https://discord.gg/XgGxMSkvUM
|
||||
|
||||
[Website](https://dioxuslabs.com) |
|
||||
[Guides](https://dioxuslabs.com/guide/) |
|
||||
[Guides](https://dioxuslabs.com/docs/0.3/guide/en/) |
|
||||
[API Docs](https://docs.rs/dioxus-hooks/latest/dioxus_hooks) |
|
||||
[Chat](https://discord.gg/XgGxMSkvUM)
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use dioxus_core::{ScopeId, ScopeState};
|
||||
use std::{
|
||||
cell::{Cell, Ref, RefCell, RefMut},
|
||||
cell::{Ref, RefCell, RefMut},
|
||||
collections::HashSet,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
|
@ -9,8 +9,8 @@ use std::{
|
|||
type ProvidedState<T> = Rc<RefCell<ProvidedStateInner<T>>>;
|
||||
|
||||
// Tracks all the subscribers to a shared State
|
||||
pub struct ProvidedStateInner<T> {
|
||||
value: Rc<RefCell<T>>,
|
||||
pub(crate) struct ProvidedStateInner<T> {
|
||||
value: T,
|
||||
notify_any: Arc<dyn Fn(ScopeId)>,
|
||||
consumers: HashSet<ScopeId>,
|
||||
}
|
||||
|
@ -21,14 +21,6 @@ impl<T> ProvidedStateInner<T> {
|
|||
(self.notify_any)(*consumer);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write(&self) -> RefMut<T> {
|
||||
self.value.borrow_mut()
|
||||
}
|
||||
|
||||
pub fn read(&self) -> Ref<T> {
|
||||
self.value.borrow()
|
||||
}
|
||||
}
|
||||
|
||||
/// This hook provides some relatively light ergonomics around shared state.
|
||||
|
@ -38,18 +30,57 @@ impl<T> ProvidedStateInner<T> {
|
|||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ## Provider
|
||||
/// ```rust
|
||||
/// # use dioxus::prelude::*;
|
||||
/// #
|
||||
/// # fn app(cx: Scope) -> Element {
|
||||
/// # render! {
|
||||
/// # Parent{}
|
||||
/// # }
|
||||
/// # }
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// #[derive(Clone, Copy)]
|
||||
/// enum Theme {
|
||||
/// Light,
|
||||
/// Dark,
|
||||
/// }
|
||||
///
|
||||
/// // Provider
|
||||
/// fn Parent<'a>(cx: Scope<'a>) -> Element<'a> {
|
||||
/// use_shared_state_provider(cx, || Theme::Dark);
|
||||
/// let theme = use_shared_state::<Theme>(cx).unwrap();
|
||||
///
|
||||
/// ```
|
||||
///
|
||||
/// ## Consumer
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// render! {
|
||||
/// button{
|
||||
/// onclick: move |_| {
|
||||
/// let current_theme = *theme.read();
|
||||
/// *theme.write() = match current_theme {
|
||||
/// Theme::Dark => Theme::Light,
|
||||
/// Theme::Light => Theme::Dark,
|
||||
/// };
|
||||
/// },
|
||||
/// "Change theme"
|
||||
/// }
|
||||
/// Child{}
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // Consumer
|
||||
/// fn Child<'a>(cx: Scope<'a>) -> Element<'a> {
|
||||
/// let theme = use_shared_state::<Theme>(cx).unwrap();
|
||||
/// let current_theme = *theme.read();
|
||||
///
|
||||
/// render! {
|
||||
/// match &*theme.read() {
|
||||
/// Theme::Dark => {
|
||||
/// "Dark mode"
|
||||
/// }
|
||||
/// Theme::Light => {
|
||||
/// "Light mode"
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # How it works
|
||||
|
@ -57,118 +88,126 @@ impl<T> ProvidedStateInner<T> {
|
|||
/// Any time a component calls `write`, every consumer of the state will be notified - excluding the provider.
|
||||
///
|
||||
/// Right now, there is not a distinction between read-only and write-only, so every consumer will be notified.
|
||||
///
|
||||
///
|
||||
///
|
||||
pub fn use_shared_state<T: 'static>(cx: &ScopeState) -> Option<UseSharedState<T>> {
|
||||
let state = cx.use_hook(|| {
|
||||
pub fn use_shared_state<T: 'static>(cx: &ScopeState) -> Option<&UseSharedState<T>> {
|
||||
let state: &Option<UseSharedStateOwner<T>> = &*cx.use_hook(move || {
|
||||
let scope_id = cx.scope_id();
|
||||
let root = cx.consume_context::<ProvidedState<T>>();
|
||||
let root = cx.consume_context::<ProvidedState<T>>()?;
|
||||
|
||||
if let Some(root) = root.as_ref() {
|
||||
root.borrow_mut().consumers.insert(scope_id);
|
||||
}
|
||||
root.borrow_mut().consumers.insert(scope_id);
|
||||
|
||||
let value = root.as_ref().map(|f| f.borrow().value.clone());
|
||||
SharedStateInner {
|
||||
root,
|
||||
value,
|
||||
scope_id,
|
||||
needs_notification: Cell::new(false),
|
||||
}
|
||||
let state = UseSharedState { inner: root };
|
||||
let owner = UseSharedStateOwner { state, scope_id };
|
||||
Some(owner)
|
||||
});
|
||||
|
||||
state.needs_notification.set(false);
|
||||
match (&state.value, &state.root) {
|
||||
(Some(value), Some(root)) => Some(UseSharedState {
|
||||
cx,
|
||||
value,
|
||||
root,
|
||||
needs_notification: &state.needs_notification,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
state.as_ref().map(|s| &s.state)
|
||||
}
|
||||
|
||||
struct SharedStateInner<T: 'static> {
|
||||
root: Option<ProvidedState<T>>,
|
||||
value: Option<Rc<RefCell<T>>>,
|
||||
/// This wrapper detects when the hook is dropped and will unsubscribe when the component is unmounted
|
||||
struct UseSharedStateOwner<T> {
|
||||
state: UseSharedState<T>,
|
||||
scope_id: ScopeId,
|
||||
needs_notification: Cell<bool>,
|
||||
}
|
||||
impl<T> Drop for SharedStateInner<T> {
|
||||
|
||||
impl<T> Drop for UseSharedStateOwner<T> {
|
||||
fn drop(&mut self) {
|
||||
// we need to unsubscribe when our component is unounted
|
||||
if let Some(root) = &self.root {
|
||||
let mut root = root.borrow_mut();
|
||||
root.consumers.remove(&self.scope_id);
|
||||
}
|
||||
// we need to unsubscribe when our component is unmounted
|
||||
let mut root = self.state.inner.borrow_mut();
|
||||
root.consumers.remove(&self.scope_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UseSharedState<'a, T: 'static> {
|
||||
pub(crate) cx: &'a ScopeState,
|
||||
pub(crate) value: &'a Rc<RefCell<T>>,
|
||||
pub(crate) root: &'a Rc<RefCell<ProvidedStateInner<T>>>,
|
||||
pub(crate) needs_notification: &'a Cell<bool>,
|
||||
/// State that is shared between components through the context system
|
||||
pub struct UseSharedState<T> {
|
||||
pub(crate) inner: Rc<RefCell<ProvidedStateInner<T>>>,
|
||||
}
|
||||
|
||||
impl<'a, T: 'static> UseSharedState<'a, T> {
|
||||
impl<T> UseSharedState<T> {
|
||||
/// Notify all consumers of the state that it has changed. (This is called automatically when you call "write")
|
||||
pub fn notify_consumers(&self) {
|
||||
self.inner.borrow_mut().notify_consumers();
|
||||
}
|
||||
|
||||
/// Read the shared value
|
||||
pub fn read(&self) -> Ref<'_, T> {
|
||||
self.value.borrow()
|
||||
}
|
||||
|
||||
pub fn notify_consumers(self) {
|
||||
if !self.needs_notification.get() {
|
||||
self.root.borrow_mut().notify_consumers();
|
||||
self.needs_notification.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_write(&self) -> (Ref<'_, T>, &Self) {
|
||||
(self.read(), self)
|
||||
Ref::map(self.inner.borrow(), |inner| &inner.value)
|
||||
}
|
||||
|
||||
/// Calling "write" will force the component to re-render
|
||||
///
|
||||
///
|
||||
/// TODO: We prevent unncessary notifications only in the hook, but we should figure out some more global lock
|
||||
// TODO: We prevent unncessary notifications only in the hook, but we should figure out some more global lock
|
||||
pub fn write(&self) -> RefMut<'_, T> {
|
||||
self.cx.needs_update();
|
||||
self.notify_consumers();
|
||||
self.value.borrow_mut()
|
||||
let mut value = self.inner.borrow_mut();
|
||||
value.notify_consumers();
|
||||
RefMut::map(value, |inner| &mut inner.value)
|
||||
}
|
||||
|
||||
/// Allows the ability to write the value without forcing a re-render
|
||||
pub fn write_silent(&self) -> RefMut<'_, T> {
|
||||
self.value.borrow_mut()
|
||||
}
|
||||
|
||||
pub fn inner(&self) -> Rc<RefCell<ProvidedStateInner<T>>> {
|
||||
self.root.clone()
|
||||
RefMut::map(self.inner.borrow_mut(), |inner| &mut inner.value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Copy for UseSharedState<'_, T> {}
|
||||
impl<'a, T> Clone for UseSharedState<'a, T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
impl<T> Clone for UseSharedState<T> {
|
||||
fn clone(&self) -> Self {
|
||||
UseSharedState {
|
||||
cx: self.cx,
|
||||
value: self.value,
|
||||
root: self.root,
|
||||
needs_notification: self.needs_notification,
|
||||
Self {
|
||||
inner: self.inner.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Provide some state for components down the hierarchy to consume without having to drill props.
|
||||
impl<T: PartialEq> PartialEq for UseSharedState<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
let first = self.inner.borrow();
|
||||
let second = other.inner.borrow();
|
||||
first.value == second.value
|
||||
}
|
||||
}
|
||||
|
||||
/// Provide some state for components down the hierarchy to consume without having to drill props. See [`use_shared_state`] to consume the state
|
||||
///
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use dioxus::prelude::*;
|
||||
/// #
|
||||
/// # fn app(cx: Scope) -> Element {
|
||||
/// # render! {
|
||||
/// # Parent{}
|
||||
/// # }
|
||||
/// # }
|
||||
///
|
||||
/// #[derive(Clone, Copy)]
|
||||
/// enum Theme {
|
||||
/// Light,
|
||||
/// Dark,
|
||||
/// }
|
||||
///
|
||||
/// // Provider
|
||||
/// fn Parent<'a>(cx: Scope<'a>) -> Element<'a> {
|
||||
/// use_shared_state_provider(cx, || Theme::Dark);
|
||||
/// let theme = use_shared_state::<Theme>(cx).unwrap();
|
||||
///
|
||||
/// render! {
|
||||
/// button{
|
||||
/// onclick: move |_| {
|
||||
/// let current_theme = *theme.read();
|
||||
/// *theme.write() = match current_theme {
|
||||
/// Theme::Dark => Theme::Light,
|
||||
/// Theme::Light => Theme::Dark,
|
||||
/// };
|
||||
/// },
|
||||
/// "Change theme"
|
||||
/// }
|
||||
/// // Children components that consume the state...
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn use_shared_state_provider<T: 'static>(cx: &ScopeState, f: impl FnOnce() -> T) {
|
||||
cx.use_hook(|| {
|
||||
let state: ProvidedState<T> = Rc::new(RefCell::new(ProvidedStateInner {
|
||||
value: Rc::new(RefCell::new(f())),
|
||||
value: f(),
|
||||
notify_any: cx.schedule_update_any(),
|
||||
consumers: HashSet::new(),
|
||||
}));
|
||||
|
|
|
@ -108,6 +108,12 @@ impl<T> Coroutine<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T> PartialEq for Coroutine<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.task == other.task
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(unused)]
|
||||
|
|
|
@ -329,7 +329,7 @@ impl PartialEq<bool> for &UseState<bool> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T: PartialEq> PartialEq<UseState<T>> for UseState<T> {
|
||||
impl<T> PartialEq<UseState<T>> for UseState<T> {
|
||||
fn eq(&self, other: &UseState<T>) -> bool {
|
||||
Rc::ptr_eq(&self.current_val, &other.current_val)
|
||||
}
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
[package]
|
||||
name = "dioxus-hot-reload"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
edition = "2021"
|
||||
license = "MIT/Apache-2.0"
|
||||
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||
homepage = "https://dioxuslabs.com"
|
||||
description = "Hot reloading utilites for Dioxus"
|
||||
documentation = "https://dioxuslabs.com"
|
||||
keywords = ["dom", "ui", "gui", "react", "hot-reloading", "watch"]
|
||||
keywords = ["dom", "ui", "gui", "react", "hot-reloading"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dioxus-rsx = { path = "../rsx" }
|
||||
dioxus-core = { path = "../core", features = ["serialize"] }
|
||||
dioxus-html = { path = "../html", features = ["hot-reload-context"] }
|
||||
dioxus-rsx = { path = "../rsx" , version = "^0.0.3" }
|
||||
dioxus-core = { path = "../core", features = ["serialize"], version = "^0.3.2"}
|
||||
dioxus-html = { path = "../html", features = ["hot-reload-context"], version = "^0.3.0" }
|
||||
|
||||
interprocess = { version = "1.2.1" }
|
||||
notify = "5.0.0"
|
||||
|
@ -23,4 +23,4 @@ serde_json = "1.0.91"
|
|||
serde = { version = "1", features = ["derive"] }
|
||||
execute = "0.2.11"
|
||||
once_cell = "1.17.0"
|
||||
ignore = "0.4.19"
|
||||
ignore = "0.4.19"
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
[discord-url]: https://discord.gg/XgGxMSkvUM
|
||||
|
||||
[Website](https://dioxuslabs.com) |
|
||||
[Guides](https://dioxuslabs.com/guide/) |
|
||||
[Guides](https://dioxuslabs.com/docs/0.3/guide/en) |
|
||||
[API Docs](https://docs.rs/dioxus-hot-reload/latest/dioxus_hot_reload) |
|
||||
[Chat](https://discord.gg/XgGxMSkvUM)
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ use std::{
|
|||
|
||||
use dioxus_core::Template;
|
||||
use dioxus_rsx::{
|
||||
hot_reload::{FileMap, UpdateResult},
|
||||
hot_reload::{FileMap, FileMapBuildResult, UpdateResult},
|
||||
HotReloadingContext,
|
||||
};
|
||||
use interprocess::local_socket::{LocalSocketListener, LocalSocketStream};
|
||||
|
@ -129,10 +129,37 @@ pub fn init<Ctx: HotReloadingContext + Send + 'static>(cfg: Config<Ctx>) {
|
|||
} = cfg;
|
||||
|
||||
if let Ok(crate_dir) = PathBuf::from_str(root_path) {
|
||||
let temp_file = std::env::temp_dir().join("@dioxusin");
|
||||
// try to find the gitingore file
|
||||
let gitignore_file_path = crate_dir.join(".gitignore");
|
||||
let (gitignore, _) = ignore::gitignore::Gitignore::new(gitignore_file_path);
|
||||
|
||||
// convert the excluded paths to absolute paths
|
||||
let excluded_paths = excluded_paths
|
||||
.iter()
|
||||
.map(|path| crate_dir.join(PathBuf::from(path)))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let channels = Arc::new(Mutex::new(Vec::new()));
|
||||
let file_map = Arc::new(Mutex::new(FileMap::<Ctx>::new(crate_dir.clone())));
|
||||
if let Ok(local_socket_stream) = LocalSocketListener::bind(temp_file.as_path()) {
|
||||
let FileMapBuildResult {
|
||||
map: file_map,
|
||||
errors,
|
||||
} = FileMap::<Ctx>::create_with_filter(crate_dir.clone(), |path| {
|
||||
// skip excluded paths
|
||||
excluded_paths.iter().any(|p| path.starts_with(p)) ||
|
||||
// respect .gitignore
|
||||
gitignore
|
||||
.matched_path_or_any_parents(path, path.is_dir())
|
||||
.is_ignore()
|
||||
})
|
||||
.unwrap();
|
||||
for err in errors {
|
||||
if log {
|
||||
println!("hot reloading failed to initialize:\n{err:?}");
|
||||
}
|
||||
}
|
||||
let file_map = Arc::new(Mutex::new(file_map));
|
||||
|
||||
if let Ok(local_socket_stream) = LocalSocketListener::bind("@dioxusin") {
|
||||
let aborted = Arc::new(Mutex::new(false));
|
||||
|
||||
// listen for connections
|
||||
|
@ -177,10 +204,6 @@ pub fn init<Ctx: HotReloadingContext + Send + 'static>(cfg: Config<Ctx>) {
|
|||
|
||||
// watch for changes
|
||||
std::thread::spawn(move || {
|
||||
// try to find the gitingore file
|
||||
let gitignore_file_path = crate_dir.join(".gitignore");
|
||||
let (gitignore, _) = ignore::gitignore::Gitignore::new(gitignore_file_path);
|
||||
|
||||
let mut last_update_time = chrono::Local::now().timestamp();
|
||||
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
|
@ -198,11 +221,6 @@ pub fn init<Ctx: HotReloadingContext + Send + 'static>(cfg: Config<Ctx>) {
|
|||
}
|
||||
}
|
||||
|
||||
let excluded_paths = excluded_paths
|
||||
.iter()
|
||||
.map(|path| crate_dir.join(PathBuf::from(path)))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut rebuild = {
|
||||
let aborted = aborted.clone();
|
||||
let channels = channels.clone();
|
||||
|
@ -242,7 +260,7 @@ pub fn init<Ctx: HotReloadingContext + Send + 'static>(cfg: Config<Ctx>) {
|
|||
matches!(
|
||||
path.extension().and_then(|p| p.to_str()),
|
||||
Some("rs" | "toml" | "css" | "html" | "js")
|
||||
)&&
|
||||
) &&
|
||||
// skip excluded paths
|
||||
!excluded_paths.iter().any(|p| path.starts_with(p)) &&
|
||||
// respect .gitignore
|
||||
|
@ -271,7 +289,7 @@ pub fn init<Ctx: HotReloadingContext + Send + 'static>(cfg: Config<Ctx>) {
|
|||
.unwrap()
|
||||
.update_rsx(path, crate_dir.as_path())
|
||||
{
|
||||
UpdateResult::UpdatedRsx(msgs) => {
|
||||
Ok(UpdateResult::UpdatedRsx(msgs)) => {
|
||||
for msg in msgs {
|
||||
let mut i = 0;
|
||||
while i < channels.len() {
|
||||
|
@ -287,13 +305,20 @@ pub fn init<Ctx: HotReloadingContext + Send + 'static>(cfg: Config<Ctx>) {
|
|||
}
|
||||
}
|
||||
}
|
||||
UpdateResult::NeedsRebuild => {
|
||||
Ok(UpdateResult::NeedsRebuild) => {
|
||||
drop(channels);
|
||||
if rebuild() {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
Err(err) => {
|
||||
if log {
|
||||
println!(
|
||||
"hot reloading failed to update rsx:\n{err:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -322,8 +347,7 @@ fn send_msg(msg: HotReloadMsg, channel: &mut impl Write) -> bool {
|
|||
/// Connect to the hot reloading listener. The callback provided will be called every time a template change is detected
|
||||
pub fn connect(mut f: impl FnMut(HotReloadMsg) + Send + 'static) {
|
||||
std::thread::spawn(move || {
|
||||
let temp_file = std::env::temp_dir().join("@dioxusin");
|
||||
if let Ok(socket) = LocalSocketStream::connect(temp_file.as_path()) {
|
||||
if let Ok(socket) = LocalSocketStream::connect("@dioxusin") {
|
||||
let mut buf_reader = BufReader::new(socket);
|
||||
loop {
|
||||
let mut buf = String::new();
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "dioxus-html"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
authors = ["Jonathan Kelley"]
|
||||
edition = "2018"
|
||||
description = "HTML Element pack for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences"
|
||||
|
@ -12,7 +12,7 @@ keywords = ["dom", "ui", "gui", "react"]
|
|||
|
||||
[dependencies]
|
||||
dioxus-core = { path = "../core", version = "^0.3.0" }
|
||||
dioxus-rsx = { path = "../rsx", version = "0.0.2", optional = true }
|
||||
dioxus-rsx = { path = "../rsx", version = "^0.0.3", optional = true }
|
||||
serde = { version = "1", features = ["derive"], optional = true }
|
||||
serde_repr = { version = "0.1", optional = true }
|
||||
wasm-bindgen = { version = "0.2.79", optional = true }
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
[discord-url]: https://discord.gg/XgGxMSkvUM
|
||||
|
||||
[Website](https://dioxuslabs.com) |
|
||||
[Guides](https://dioxuslabs.com/guide/) |
|
||||
[Guides](https://dioxuslabs.com/docs/0.3/guide/en/) |
|
||||
[API Docs](https://docs.rs/dioxus-html/latest/dioxus_html) |
|
||||
[Chat](https://discord.gg/XgGxMSkvUM)
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "dioxus-interpreter-js"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
edition = "2018"
|
||||
authors = ["Jonathan Kelley"]
|
||||
description = "JS Intepreter for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences"
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
[discord-url]: https://discord.gg/XgGxMSkvUM
|
||||
|
||||
[Website](https://dioxuslabs.com) |
|
||||
[Guides](https://dioxuslabs.com/guide/) |
|
||||
[Guides](https://dioxuslabs.com/docs/0.3/guide/en/) |
|
||||
[API Docs](https://docs.rs/dioxus-interpreter-js/latest/dioxus_interpreter_js) |
|
||||
[Chat](https://discord.gg/XgGxMSkvUM)
|
||||
|
||||
|
|
|
@ -116,9 +116,6 @@ class Interpreter {
|
|||
node.remove();
|
||||
}
|
||||
}
|
||||
CreateRawText(text) {
|
||||
this.stack.push(document.createTextNode(text));
|
||||
}
|
||||
CreateTextNode(text, root) {
|
||||
const node = document.createTextNode(text);
|
||||
this.nodes[root] = node;
|
||||
|
@ -306,7 +303,7 @@ class Interpreter {
|
|||
this.CreatePlaceholder(edit.id);
|
||||
break;
|
||||
case "CreateTextNode":
|
||||
this.CreateTextNode(edit.value);
|
||||
this.CreateTextNode(edit.value, edit.id);
|
||||
break;
|
||||
case "HydrateText":
|
||||
this.HydrateText(edit.path, edit.value, edit.id);
|
||||
|
@ -360,10 +357,11 @@ class Interpreter {
|
|||
|
||||
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");
|
||||
let a_element = target.closest("a");
|
||||
if (a_element != null) {
|
||||
event.preventDefault();
|
||||
if (shouldPreventDefault !== `onclick` && a_element.getAttribute(`dioxus-prevent-default`) !== `onclick`) {
|
||||
const href = a_element.getAttribute("href");
|
||||
if (href !== "" && href !== null && href !== undefined) {
|
||||
window.ipc.postMessage(
|
||||
serializeIpcMessage("browser_open", { href })
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
[discord-url]: https://discord.gg/XgGxMSkvUM
|
||||
|
||||
[Website](https://dioxuslabs.com) |
|
||||
[Guides](https://dioxuslabs.com/guide/) |
|
||||
[Guides](https://dioxuslabs.com/docs/0.3/guide/en/) |
|
||||
[API Docs](https://docs.rs/dioxus-liveview/latest/dioxus_liveview) |
|
||||
[Chat](https://discord.gg/XgGxMSkvUM)
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ async fn main() {
|
|||
r#"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head> <title>Dioxus LiveView with Warp</title> </head>
|
||||
<head> <title>Dioxus LiveView with axum</title> </head>
|
||||
<body> <div id="main"></div> </body>
|
||||
{glue}
|
||||
</html>
|
||||
|
|
|
@ -41,7 +41,7 @@ fn index(_depot: &mut Depot, res: &mut Response) {
|
|||
r#"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head> <title>Dioxus LiveView with Warp</title> </head>
|
||||
<head> <title>Dioxus LiveView with Salvo</title> </head>
|
||||
<body> <div id="main"></div> </body>
|
||||
{glue}
|
||||
</html>
|
||||
|
|
|
@ -37,7 +37,16 @@ impl LiveViewPool {
|
|||
app: fn(Scope<T>) -> Element,
|
||||
props: T,
|
||||
) -> Result<(), LiveViewError> {
|
||||
match self.pool.spawn_pinned(move || run(app, props, ws)).await {
|
||||
self.launch_virtualdom(ws, move || VirtualDom::new_with_props(app, props))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn launch_virtualdom<F: FnOnce() -> VirtualDom + Send + 'static>(
|
||||
&self,
|
||||
ws: impl LiveViewSocket,
|
||||
make_app: F,
|
||||
) -> Result<(), LiveViewError> {
|
||||
match self.pool.spawn_pinned(move || run(make_app(), ws)).await {
|
||||
Ok(Ok(_)) => Ok(()),
|
||||
Ok(Err(e)) => Err(e),
|
||||
Err(_) => Err(LiveViewError::SendingFailed),
|
||||
|
@ -95,14 +104,7 @@ impl<S> LiveViewSocket for S where
|
|||
/// As long as your framework can provide a Sink and Stream of Strings, you can use this function.
|
||||
///
|
||||
/// You might need to transform the error types of the web backend into the LiveView error type.
|
||||
pub async fn run<T>(
|
||||
app: Component<T>,
|
||||
props: T,
|
||||
ws: impl LiveViewSocket,
|
||||
) -> Result<(), LiveViewError>
|
||||
where
|
||||
T: Send + 'static,
|
||||
{
|
||||
pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), LiveViewError> {
|
||||
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
||||
let mut hot_reload_rx = {
|
||||
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
|
@ -112,8 +114,6 @@ where
|
|||
rx
|
||||
};
|
||||
|
||||
let mut vdom = VirtualDom::new_with_props(app, props);
|
||||
|
||||
// todo: use an efficient binary packed format for this
|
||||
let edits = serde_json::to_string(&vdom.rebuild()).unwrap();
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
[discord-url]: https://discord.gg/XgGxMSkvUM
|
||||
|
||||
[Website](https://dioxuslabs.com) |
|
||||
[Guides](https://dioxuslabs.com/guide/) |
|
||||
[Guides](https://dioxuslabs.com/docs/0.3/guide/en/) |
|
||||
[API Docs](https://docs.rs/dioxus-mobile/latest/dioxus_mobile) |
|
||||
[Chat](https://discord.gg/XgGxMSkvUM)
|
||||
|
||||
|
@ -98,7 +98,7 @@ To configure the web view, menubar, and other important desktop-specific feature
|
|||
|
||||
## Future Steps
|
||||
|
||||
Make sure to read the [Dioxus Guide](https://dioxuslabs.com/guide) if you already haven't!
|
||||
Make sure to read the [Dioxus Guide](https://dioxuslabs.com/docs/0.3/guide/en) if you already haven't!
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Dioxus Native Core
|
||||
# Dioxus Router
|
||||
|
||||
[![Crates.io][crates-badge]][crates-url]
|
||||
[![MIT licensed][mit-badge]][mit-url]
|
||||
|
@ -18,7 +18,7 @@
|
|||
[discord-url]: https://discord.gg/XgGxMSkvUM
|
||||
|
||||
[Website](https://dioxuslabs.com) |
|
||||
[Guides](https://dioxuslabs.com/guide/) |
|
||||
[Guides](https://dioxuslabs.com/docs/0.3/guide/en/) |
|
||||
[API Docs](https://docs.rs/dioxus-router/latest/dioxus_router) |
|
||||
[Chat](https://discord.gg/XgGxMSkvUM)
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ edition = "2018"
|
|||
|
||||
[dependencies]
|
||||
dioxus-autofmt = { path = "../autofmt" }
|
||||
dioxus-rsx = { path = "../rsx" }
|
||||
dioxus-rsx = { path = "../rsx" , version = "^0.0.3" }
|
||||
html_parser = "0.6.3"
|
||||
proc-macro2 = "1.0.49"
|
||||
quote = "1.0.23"
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
[discord-url]: https://discord.gg/XgGxMSkvUM
|
||||
|
||||
[Website](https://dioxuslabs.com) |
|
||||
[Guides](https://dioxuslabs.com/guide/) |
|
||||
[Guides](https://dioxuslabs.com/docs/0.3/guide/en/) |
|
||||
[API Docs](https://docs.rs/rsx-rosetta/latest/rsx-rosetta) |
|
||||
[Chat](https://discord.gg/XgGxMSkvUM)
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "dioxus-rsx"
|
||||
version = "0.0.2"
|
||||
version = "0.0.3"
|
||||
edition = "2018"
|
||||
license = "MIT/Apache-2.0"
|
||||
description = "Core functionality for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences"
|
||||
|
@ -13,8 +13,9 @@ keywords = ["dom", "ui", "gui", "react"]
|
|||
|
||||
[dependencies]
|
||||
proc-macro2 = { version = "1.0", features = ["span-locations"] }
|
||||
dioxus-core = { path = "../core", version = "0.3.0"}
|
||||
dioxus-core = { path = "../core", version = "^0.3.0"}
|
||||
syn = { version = "1.0", features = ["full", "extra-traits"] }
|
||||
quote = { version = "1.0" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
internment = "0.7.0"
|
||||
krates = "0.12.6"
|
|
@ -18,7 +18,7 @@
|
|||
[discord-url]: https://discord.gg/XgGxMSkvUM
|
||||
|
||||
[Website](https://dioxuslabs.com) |
|
||||
[Guides](https://dioxuslabs.com/guide/) |
|
||||
[Guides](https://dioxuslabs.com/docs/0.3/guide/en/) |
|
||||
[API Docs](https://docs.rs/dioxus-rsx/latest/dioxus_rsx) |
|
||||
[Chat](https://discord.gg/XgGxMSkvUM)
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
use crate::{CallBody, HotReloadingContext};
|
||||
use dioxus_core::Template;
|
||||
use krates::cm::MetadataCommand;
|
||||
use krates::Cmd;
|
||||
pub use proc_macro2::TokenStream;
|
||||
pub use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
@ -17,46 +19,82 @@ pub enum UpdateResult {
|
|||
NeedsRebuild,
|
||||
}
|
||||
|
||||
/// The result of building a FileMap
|
||||
pub struct FileMapBuildResult<Ctx: HotReloadingContext> {
|
||||
/// The FileMap that was built
|
||||
pub map: FileMap<Ctx>,
|
||||
/// Any errors that occurred while building the FileMap that were not fatal
|
||||
pub errors: Vec<io::Error>,
|
||||
}
|
||||
|
||||
pub struct FileMap<Ctx: HotReloadingContext> {
|
||||
pub map: HashMap<PathBuf, (String, Option<Template<'static>>)>,
|
||||
in_workspace: HashMap<PathBuf, bool>,
|
||||
phantom: std::marker::PhantomData<Ctx>,
|
||||
}
|
||||
|
||||
impl<Ctx: HotReloadingContext> FileMap<Ctx> {
|
||||
/// Create a new FileMap from a crate directory
|
||||
pub fn new(path: PathBuf) -> Self {
|
||||
pub fn create(path: PathBuf) -> io::Result<FileMapBuildResult<Ctx>> {
|
||||
Self::create_with_filter(path, |_| false)
|
||||
}
|
||||
|
||||
/// Create a new FileMap from a crate directory
|
||||
pub fn create_with_filter(
|
||||
path: PathBuf,
|
||||
mut filter: impl FnMut(&Path) -> bool,
|
||||
) -> io::Result<FileMapBuildResult<Ctx>> {
|
||||
struct FileMapSearchResult {
|
||||
map: HashMap<PathBuf, (String, Option<Template<'static>>)>,
|
||||
errors: Vec<io::Error>,
|
||||
}
|
||||
fn find_rs_files(
|
||||
root: PathBuf,
|
||||
) -> io::Result<HashMap<PathBuf, (String, Option<Template<'static>>)>> {
|
||||
filter: &mut impl FnMut(&Path) -> bool,
|
||||
) -> io::Result<FileMapSearchResult> {
|
||||
let mut files = HashMap::new();
|
||||
let mut errors = Vec::new();
|
||||
if root.is_dir() {
|
||||
for entry in (fs::read_dir(root)?).flatten() {
|
||||
let path = entry.path();
|
||||
files.extend(find_rs_files(path)?);
|
||||
if !filter(&path) {
|
||||
let FileMapSearchResult {
|
||||
map,
|
||||
errors: child_errors,
|
||||
} = find_rs_files(path, filter)?;
|
||||
errors.extend(child_errors);
|
||||
files.extend(map);
|
||||
}
|
||||
}
|
||||
} else if root.extension().and_then(|s| s.to_str()) == Some("rs") {
|
||||
if let Ok(mut file) = File::open(root.clone()) {
|
||||
let mut src = String::new();
|
||||
file.read_to_string(&mut src).expect("Unable to read file");
|
||||
file.read_to_string(&mut src)?;
|
||||
files.insert(root, (src, None));
|
||||
}
|
||||
}
|
||||
Ok(files)
|
||||
Ok(FileMapSearchResult { map: files, errors })
|
||||
}
|
||||
|
||||
let FileMapSearchResult { map, errors } = find_rs_files(path, &mut filter)?;
|
||||
let result = Self {
|
||||
map: find_rs_files(path).unwrap(),
|
||||
map,
|
||||
in_workspace: HashMap::new(),
|
||||
phantom: std::marker::PhantomData,
|
||||
};
|
||||
result
|
||||
Ok(FileMapBuildResult {
|
||||
map: result,
|
||||
errors,
|
||||
})
|
||||
}
|
||||
|
||||
/// Try to update the rsx in a file
|
||||
pub fn update_rsx(&mut self, file_path: &Path, crate_dir: &Path) -> UpdateResult {
|
||||
let mut file = File::open(file_path).unwrap();
|
||||
pub fn update_rsx(&mut self, file_path: &Path, crate_dir: &Path) -> io::Result<UpdateResult> {
|
||||
let mut file = File::open(file_path)?;
|
||||
let mut src = String::new();
|
||||
file.read_to_string(&mut src).expect("Unable to read file");
|
||||
file.read_to_string(&mut src)?;
|
||||
if let Ok(syntax) = syn::parse_file(&src) {
|
||||
let in_workspace = self.child_in_workspace(crate_dir)?;
|
||||
if let Some((old_src, template_slot)) = self.map.get_mut(file_path) {
|
||||
if let Ok(old) = syn::parse_file(old_src) {
|
||||
match find_rsx(&syntax, &old) {
|
||||
|
@ -72,7 +110,17 @@ impl<Ctx: HotReloadingContext> FileMap<Ctx> {
|
|||
syn::parse2::<CallBody>(old.tokens),
|
||||
syn::parse2::<CallBody>(new),
|
||||
) {
|
||||
if let Ok(file) = file_path.strip_prefix(crate_dir) {
|
||||
// if the file!() macro is invoked in a workspace, the path is relative to the workspace root, otherwise it's relative to the crate root
|
||||
// we need to check if the file is in a workspace or not and strip the prefix accordingly
|
||||
let prefix = if in_workspace {
|
||||
crate_dir.parent().ok_or(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Could not load workspace",
|
||||
))?
|
||||
} else {
|
||||
crate_dir
|
||||
};
|
||||
if let Ok(file) = file_path.strip_prefix(prefix) {
|
||||
let line = old_start.line;
|
||||
let column = old_start.column + 1;
|
||||
let location = file.display().to_string()
|
||||
|
@ -91,7 +139,7 @@ impl<Ctx: HotReloadingContext> FileMap<Ctx> {
|
|||
{
|
||||
// dioxus cannot handle empty templates
|
||||
if template.roots.is_empty() {
|
||||
return UpdateResult::NeedsRebuild;
|
||||
return Ok(UpdateResult::NeedsRebuild);
|
||||
} else {
|
||||
// if the template is the same, don't send it
|
||||
if let Some(old_template) = template_slot {
|
||||
|
@ -103,20 +151,44 @@ impl<Ctx: HotReloadingContext> FileMap<Ctx> {
|
|||
messages.push(template);
|
||||
}
|
||||
} else {
|
||||
return UpdateResult::NeedsRebuild;
|
||||
return Ok(UpdateResult::NeedsRebuild);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return UpdateResult::UpdatedRsx(messages);
|
||||
return Ok(UpdateResult::UpdatedRsx(messages));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// if this is a new file, rebuild the project
|
||||
*self = FileMap::new(crate_dir.to_path_buf());
|
||||
let FileMapBuildResult { map, mut errors } =
|
||||
FileMap::create(crate_dir.to_path_buf())?;
|
||||
if let Some(err) = errors.pop() {
|
||||
return Err(err);
|
||||
}
|
||||
*self = map;
|
||||
}
|
||||
}
|
||||
UpdateResult::NeedsRebuild
|
||||
Ok(UpdateResult::NeedsRebuild)
|
||||
}
|
||||
|
||||
fn child_in_workspace(&mut self, crate_dir: &Path) -> io::Result<bool> {
|
||||
if let Some(in_workspace) = self.in_workspace.get(crate_dir) {
|
||||
Ok(*in_workspace)
|
||||
} else {
|
||||
let mut cmd = Cmd::new();
|
||||
let manafest_path = crate_dir.join("Cargo.toml");
|
||||
cmd.manifest_path(&manafest_path);
|
||||
let cmd: MetadataCommand = cmd.into();
|
||||
let metadata = cmd
|
||||
.exec()
|
||||
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))?;
|
||||
|
||||
let in_workspace = metadata.workspace_root != crate_dir;
|
||||
self.in_workspace
|
||||
.insert(crate_dir.to_path_buf(), in_workspace);
|
||||
Ok(in_workspace)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ keywords = ["dom", "ui", "gui", "react", "ssr"]
|
|||
|
||||
[dependencies]
|
||||
dioxus-core = { path = "../core", version = "^0.3.0", features = ["serialize"] }
|
||||
askama_escape = "0.10.3"
|
||||
|
||||
[dev-dependencies]
|
||||
dioxus = { path = "../dioxus", version = "0.3.0" }
|
||||
|
|
|
@ -8,10 +8,9 @@
|
|||
## Resources
|
||||
This crate is a part of the broader Dioxus ecosystem. For more resources about Dioxus, check out:
|
||||
|
||||
- [Getting Started](https://dioxuslabs.com/getting-started)
|
||||
- [Book](https://dioxuslabs.com/book)
|
||||
- [Reference](https://dioxuslabs.com/reference)
|
||||
- [Community Examples](https://github.com/DioxusLabs/community-examples)
|
||||
- [Getting Started](https://dioxuslabs.com/docs/0.3/guide/en/getting_started/index.html)
|
||||
- [Book](https://dioxuslabs.com/docs/0.3/guide/en/)
|
||||
- [Examples](https://github.com/DioxusLabs/example-projects)
|
||||
|
||||
## Overview
|
||||
|
||||
|
|
|
@ -82,7 +82,13 @@ impl StringCache {
|
|||
}
|
||||
cur_path.pop();
|
||||
}
|
||||
TemplateNode::Text { text } => write!(chain, "{text}")?,
|
||||
TemplateNode::Text { text } => {
|
||||
write!(
|
||||
chain,
|
||||
"{}",
|
||||
askama_escape::escape(text, askama_escape::Html)
|
||||
)?;
|
||||
}
|
||||
TemplateNode::Dynamic { id: idx } | TemplateNode::DynamicText { id: idx } => {
|
||||
chain.segments.push(Segment::Node(*idx))
|
||||
}
|
||||
|
|
|
@ -104,8 +104,11 @@ impl Renderer {
|
|||
write!(buf, "<!--#-->")?;
|
||||
}
|
||||
|
||||
// todo: escape the text
|
||||
write!(buf, "{}", text.value)?;
|
||||
write!(
|
||||
buf,
|
||||
"{}",
|
||||
askama_escape::escape(text.value, askama_escape::Html)
|
||||
)?;
|
||||
|
||||
if self.pre_render {
|
||||
write!(buf, "<!--#-->")?;
|
||||
|
@ -119,7 +122,7 @@ impl Renderer {
|
|||
|
||||
DynamicNode::Placeholder(_el) => {
|
||||
if self.pre_render {
|
||||
write!(buf, "<pre><pre/>")?;
|
||||
write!(buf, "<pre></pre>")?;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -138,7 +141,7 @@ fn to_string_works() {
|
|||
|
||||
fn app(cx: Scope) -> Element {
|
||||
let dynamic = 123;
|
||||
let dyn2 = "</diiiiiiiiv>"; // todo: escape this
|
||||
let dyn2 = "</diiiiiiiiv>"; // this should be escaped
|
||||
|
||||
render! {
|
||||
div { class: "asdasdasd", class: "asdasdasd", id: "id-{dynamic}",
|
||||
|
@ -165,10 +168,10 @@ fn to_string_works() {
|
|||
vec![
|
||||
PreRendered("<div class=\"asdasdasd\" class=\"asdasdasd\"".into(),),
|
||||
Attr(0,),
|
||||
PreRendered(">Hello world 1 -->".into(),),
|
||||
PreRendered(">Hello world 1 -->".into(),),
|
||||
Node(0,),
|
||||
PreRendered(
|
||||
"<-- Hello world 2<div>nest 1</div><div></div><div>nest 2</div>".into(),
|
||||
"<-- Hello world 2<div>nest 1</div><div></div><div>nest 2</div>".into(),
|
||||
),
|
||||
Node(1,),
|
||||
Node(2,),
|
||||
|
@ -180,5 +183,5 @@ fn to_string_works() {
|
|||
|
||||
use Segment::*;
|
||||
|
||||
assert_eq!(out, "<div class=\"asdasdasd\" class=\"asdasdasd\" id=\"id-123\">Hello world 1 -->123<-- Hello world 2<div>nest 1</div><div></div><div>nest 2</div></diiiiiiiiv><div>finalize 0</div><div>finalize 1</div><div>finalize 2</div><div>finalize 3</div><div>finalize 4</div></div>");
|
||||
assert_eq!(out, "<div class=\"asdasdasd\" class=\"asdasdasd\" id=\"id-123\">Hello world 1 -->123<-- Hello world 2<div>nest 1</div><div></div><div>nest 2</div></diiiiiiiiv><div>finalize 0</div><div>finalize 1</div><div>finalize 2</div><div>finalize 3</div><div>finalize 4</div></div>");
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ fn dynamic() {
|
|||
dioxus_ssr::render_lazy(rsx! {
|
||||
div { "Hello world 1 -->" "{dynamic}" "<-- Hello world 2" }
|
||||
}),
|
||||
"<div>Hello world 1 -->123<-- Hello world 2</div>"
|
||||
"<div>Hello world 1 -->123<-- Hello world 2</div>"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "dioxus-web"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
authors = ["Jonathan Kelley"]
|
||||
edition = "2018"
|
||||
description = "Dioxus VirtualDOM renderer for the web browser using websys"
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
[discord-url]: https://discord.gg/XgGxMSkvUM
|
||||
|
||||
[Website](https://dioxuslabs.com) |
|
||||
[Guides](https://dioxuslabs.com/guide/) |
|
||||
[Guides](https://dioxuslabs.com/docs/0.3/guide/en/) |
|
||||
[API Docs](https://docs.rs/dioxus-web/latest/dioxus_web) |
|
||||
[Chat](https://discord.gg/XgGxMSkvUM)
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
<span> | </span>
|
||||
<a href="https://github.com/DioxusLabs/example-projects"> Exemplos </a>
|
||||
<span> | </span>
|
||||
<a href="https://dioxuslabs.com/guide"> Guia </a>
|
||||
<a href="https://dioxuslabs.com/guide/pt-br"> Guia </a>
|
||||
<span> | </span>
|
||||
<a href="https://github.com/DioxusLabs/dioxus/blob/master/notes/README/ZH_CN.md"> 中文 </a>
|
||||
</h3>
|
||||
|
@ -83,7 +83,7 @@ Se você conhece React, então você já conhece o Dioxus.
|
|||
|
||||
<table style="width:100%" align="center">
|
||||
<tr>
|
||||
<th><a href="https://dioxuslabs.com/guide/">Tutorial</a></th>
|
||||
<th><a href="https://dioxuslabs.com/guide/pt-br/">Tutorial</a></th>
|
||||
<th><a href="https://dioxuslabs.com/reference/web">Web</a></th>
|
||||
<th><a href="https://dioxuslabs.com/reference/desktop/">Desktop</a></th>
|
||||
<th><a href="https://dioxuslabs.com/reference/ssr/">SSR</a></th>
|
||||
|
@ -159,7 +159,7 @@ Dioxus é único no ecossistema do Rust por suportar:
|
|||
- SSR com `hydration` feito pelo Cliente
|
||||
- Suporte à aplicação Desktop
|
||||
|
||||
Para mais informações sobre quais funções estão atualmente disponíveis e para o progresso futuro, veja [O Guia](https://dioxuslabs.com/guide/).
|
||||
Para mais informações sobre quais funções estão atualmente disponíveis e para o progresso futuro, veja [O Guia](https://dioxuslabs.com/guide/pt-br/).
|
||||
|
||||
## Projeto dentro do ecossistema Dioxus
|
||||
|
||||
|
|
Loading…
Reference in a new issue