Merge pull request #38 from DioxusLabs/jk/stack_dst
wip: docs and router
This commit is contained in:
95 files changed
+2457
-8761
No files matched your search
@@ -91,3 +91,7 @@ path = "./examples/webview.rs"
|
||||
required-features = ["desktop"]
|
||||
name = "tailwind"
|
||||
path = "./examples/tailwind.rs"
|
||||
|
||||
|
||||
# [patch.crates-io]
|
||||
# wasm-bindgen = { path = "../Tinkering/wasm-bindgen" }
|
||||
@@ -49,7 +49,7 @@
|
||||
Dioxus is a portable, performant, and ergonomic framework for building cross-platform user experiences in Rust.
|
||||
|
||||
```rust
|
||||
fn App(cx: Context, props: &()) -> Element {
|
||||
fn App(cx: Scope, props: &()) -> Element {
|
||||
let mut count = use_state(cx, || 0);
|
||||
|
||||
cx.render(rsx!(
|
||||
@@ -65,12 +65,11 @@ Dioxus can be used to deliver webapps, desktop apps, static sites, liveview apps
|
||||
If you know React, then you already know Dioxus.
|
||||
|
||||
### Unique features:
|
||||
- The most ergonomic and powerful state management of any Rust UI toolkit.
|
||||
- Desktop apps running natively (no Electron!) in less than 10 lines of code.
|
||||
- Starting a new app takes zero templates or special tools - get a new app running in just seconds.
|
||||
- Incredible inline documentation. Supports hover and guides for all HTML elements, listeners, and events.
|
||||
- Custom bump-allocator backing for all components. Nearly 0 allocations for steady-state components.
|
||||
- Multithreaded asynchronous coroutine scheduler for powerful async code.
|
||||
- Incredibly ergonomic and powerful state management.
|
||||
- Incredible inline documentation - hover and guides for all HTML elements, listeners, and events.
|
||||
- Extremely memory efficient - 0 global allocations for steady-state components.
|
||||
- Multithreaded asynchronous coroutine scheduler for first-class async support.
|
||||
- And more! Read the full release post here.
|
||||
|
||||
## Get Started with...
|
||||
@@ -95,24 +94,42 @@ If you know React, then you already know Dioxus.
|
||||
|
||||
See the awesome-dioxus page for a curated list of content in the Dioxus Ecosystem.
|
||||
|
||||
## Why?
|
||||
## Why Dioxus and why Rust?
|
||||
|
||||
TypeScript is a great addition to JavaScript, but comes with a lot of tweaking flags, a slight performance hit, and an uneven ecosystem where some of the most important packages are not properly typed. TypeScript provides a lot of great benefits to JS projects, but comes with its own "tax" that can slow down dev teams. Rust can be seen as a step up from TypeScript, supporting:
|
||||
TypeScript is a fantastic addition to JavaScript, but it's still fundamentally JavaScript. TS code runs slightly slower, has tons of configuration options, and not every package is properly typed.
|
||||
|
||||
- static types for _all_ libraries
|
||||
- advanced pattern matching
|
||||
- immutability by default
|
||||
- clean, composable iterators
|
||||
- a good module system
|
||||
- integrated documentation
|
||||
- inline built-in unit/integration testing
|
||||
- best-in-class error handling
|
||||
- simple and fast build system (compared to WebPack!)
|
||||
- powerful standard library (no need for lodash or underscore)
|
||||
- include_str! for integrating html/css/svg templates directly
|
||||
- various macros (`html!`, `rsx!`) for fast template iteration
|
||||
In contrast, Dioxus is written in Rust - which is almost like "TypeScript on steroids".
|
||||
|
||||
And much more. Dioxus makes Rust apps just as fast to write as React apps, but affords more robustness, giving your frontend team greater confidence in making big changes in shorter time. Dioxus also works on the server, on the web, on mobile, on desktop - and it runs completely natively so performance is never an issue.
|
||||
By using Rust, we gain:
|
||||
|
||||
- Static types for *every* library
|
||||
- Immutability by default
|
||||
- A simple and intuitive module system
|
||||
- Integrated documentation (`go to source` _actually goes to source_)
|
||||
- Advanced pattern matching
|
||||
- Clean, efficient, composable iterators
|
||||
- Inline built-in unit/integration testing
|
||||
- Best-in-class error handling
|
||||
- Powerful and sane standard library
|
||||
- Flexible macro system
|
||||
- Access to `crates.io`
|
||||
|
||||
Specifically, Dioxus provides us many other assurances:
|
||||
|
||||
- Proper use of immutable datastructures
|
||||
- Guaranteed error handling (so you can sleep easy at night not worrying about `cannot read property of undefined`)
|
||||
- Native performance on mobile
|
||||
- Direct access to system IO
|
||||
|
||||
And much more. Dioxus makes Rust apps just as fast to write as React apps, but affords more robustness, giving your frontend team greater confidence in making big changes in shorter time.
|
||||
|
||||
### Why NOT Dioxus?
|
||||
You shouldn't use Dioxus if:
|
||||
|
||||
- You don't like the React Hooks approach to frontend
|
||||
- You need a no-std renderer
|
||||
- You want to support browsers where Wasm or asm.js are not supported.
|
||||
- You need a Send+Sync UI solution (Dioxus is not currently ThreadSafe)
|
||||
|
||||
# Parity with React
|
||||
|
||||
@@ -158,189 +175,6 @@ Dioxus is heavily inspired by React, but we want your transition to feel like an
|
||||
- 👀 = not yet implemented or being worked on
|
||||
- ❓ = not sure if will or can implement
|
||||
|
||||
## FAQ:
|
||||
|
||||
### Aren't VDOMs just pure overhead? Why not something like Solid or Svelte?
|
||||
Remember: Dioxus is a library - not a compiler like Svelte. Plus, the inner VirtualDOM allows Dioxus to easily port into different runtimes, support SSR, and run remotely in the cloud. VDOMs tend to more ergonomic to work with and feel roughly like natural Rust code. The overhead of Dioxus is **extraordinarily** minimal... sure, there may be some overhead but on an order of magnitude lower than the time required to actually update the page.
|
||||
|
||||
|
||||
### Isn't the overhead for interacting with the DOM from Wasm too much?
|
||||
The overhead layer between Wasm and JS APIs is extremely poorly understood. Rust web benchmarks typically suffer from differences in how Rust and JS cache strings. In Dioxus, we solve most of these issues and our JS Framework Benchmark actually beats the Wasm Bindgen benchmark in many cases. Compared to a "pure vanilla JS" solution, Dioxus adds less than 5% of overhead and takes advantage of batched DOM manipulation.
|
||||
|
||||
### Aren't Wasm binaries too huge to deploy in production?
|
||||
Wasm binary sizes are another poorly understood characteristic of Rust web apps. 50kb of Wasm and 50kb of JS are _not_ made equally. In JS, the code must be downloaded _first_ and _then_ JIT-ted. Just-in-time compiling 50kb of JavaScript takes a while which is why 50kb of JavaScript sounds like a lot! However, with Wasm, the code is downloaded and JIT-ted _simultaneously_ through the magic of streaming compilation. By the time the 50kb of Rust is finished downloading, it is already ready to go. Again, Dioxus beats out many benchmarks with time-to-interactivity.
|
||||
|
||||
For reference, Dioxus `hello-world` clocks in at around 70kb gzip or 60kb brotli, and Dioxus supports SSR.
|
||||
|
||||
### Why hooks? Why not MVC, classes, traits, messages, etc?
|
||||
There are plenty Rust Elm-like frameworks in the world - we were not interested in making another! Instead, we borrowed hooks from React. JS and Rust share many structural similarities, so if you're comfortable with React, then you'll be plenty comfortable with Dioxus.
|
||||
|
||||
### Why a custom DSL? Why not just pure function calls?
|
||||
The `RSX` DSL is _barely_ a DSL. Rustaceans will find the DSL very similar to simply assembling nested structs, but without the syntactical overhead of "Default" everywhere or having to jump through hoops with the builder pattern. Between RSX, HTML, the Raw Factory API, and the NodeBuilder syntax, there's plenty of options to choose from.
|
||||
|
||||
### What are the build times like? Why on earth would I choose Rust instead of JS/TS/Elm?
|
||||
Dioxus builds as roughly as fast as a complex WebPack-TypeScript site. Compile times will be slower than an equivalent TypeScript site, but not unbearably slow. The Wasm compiler backend for Rust is very fast. Iterating on small components is basically instant and larger apps takes a few seconds. In practice, the compiler guarantees of Rust balance out the rebuild times.
|
||||
|
||||
### What about Yew/Seed/Sycamore/Dominator/Dodrio/Percy?
|
||||
- Yew and Seed use an Elm-like pattern and don't support SSR or any alternate rendering platforms
|
||||
- Sycamore and Dominator are more like SolidJS/Svelte, requiring no VDOM but has less naturally-Rusty state management
|
||||
- Percy isn't quite mature yet
|
||||
- Dodrio is the spiritual predecessor of Dioxus, but is currently an archived research project without the batteries of Dioxus
|
||||
|
||||
### How do the mobile and desktop renderers work? Is it Electron?
|
||||
Currently, Dioxus uses your device's native WebView library to draw the page. None of your app code is actually running in the WebView thread, so you can access system resources instead of having to go through something like NodeJS. This means your app will use Safari on macOS/iOS, Edge (Chromium) on Windows, and whatever is the default Web Browser for Linux and Android. Because your code is compiled and running natively, performance is not a problem. You will have to use the various "Escape Hatches" to use browser-native APIs (like WebGL) and work around visual differences in how Safari and Chrome render the page.
|
||||
|
||||
In the future, we are interested in using Webrenderer to provide a fully native renderer without having to go through the system WebView library. In practice, Dioxus mobile and desktop are great for CRUD-style apps, but the ergonomic cross-platform APIs (GPS, Camera, etc) are not there yet.
|
||||
|
||||
### Why NOT Dioxus?
|
||||
You shouldn't use Dioxus if:
|
||||
- You don't like the React Hooks approach to frontend
|
||||
- You need a no-std renderer
|
||||
- You want to support browsers where Wasm or asm.js are not supported.
|
||||
|
||||
|
||||
## Show me some examples!
|
||||
|
||||
In our collection of examples, guides, and tutorials, we have:
|
||||
- The book (an introductory course)
|
||||
- The guide (an in-depth analysis of everything in Dioxus)
|
||||
- The reference (a collection of examples with heavy documentation)
|
||||
- The general examples
|
||||
- The platform-specific examples (web, ssr, desktop, mobile, server)
|
||||
|
||||
Here's what a few common tasks look like in Dioxus:
|
||||
|
||||
Nested components with children and internal state:
|
||||
```rust
|
||||
fn App(cx: Context, props: &()) -> Element {
|
||||
cx.render(rsx!( Toggle { "Toggle me" } ))
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Props)]
|
||||
struct ToggleProps { children: Element }
|
||||
|
||||
fn Toggle(cx: Context, props: &ToggleProps) -> Element {
|
||||
let mut toggled = use_state(cx, || false);
|
||||
cx.render(rsx!{
|
||||
div {
|
||||
{&props.children}
|
||||
button { onclick: move |_| toggled.set(true),
|
||||
{toggled.and_then(|| "On").or_else(|| "Off")}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Controlled inputs:
|
||||
```rust
|
||||
fn App(cx: Context, props: &()) -> Element {
|
||||
let value = use_state(cx, String::new);
|
||||
cx.render(rsx!(
|
||||
input {
|
||||
"type": "text",
|
||||
value: "{value}",
|
||||
oninput: move |evt| value.set(evt.value.clone())
|
||||
}
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
Lists and Conditional rendering:
|
||||
```rust
|
||||
fn App(cx: Context, props: &()) -> Element {
|
||||
let list = (0..10).map(|i| {
|
||||
rsx!(li { key: "{i}", "Value: {i}" })
|
||||
});
|
||||
|
||||
let title = match list.len() {
|
||||
0 => rsx!("Not enough"),
|
||||
_ => rsx!("Plenty!"),
|
||||
};
|
||||
|
||||
if should_show {
|
||||
cx.render(rsx!(
|
||||
{title}
|
||||
ul { {list} }
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Tiny components:
|
||||
```rust
|
||||
static App: FC<()> = |cx, _| rsx!(cx, div {"hello world!"});
|
||||
```
|
||||
|
||||
Borrowed prop contents:
|
||||
```rust
|
||||
fn App(cx: Context, props: &()) -> Element {
|
||||
let name = use_state(cx, || String::from("example"));
|
||||
rsx!(cx, Child { title: name.as_str() })
|
||||
}
|
||||
|
||||
#[derive(Props)]
|
||||
struct ChildProps<'a> { title: &'a str }
|
||||
|
||||
fn Child(cx: Context, props: &ChildProps) -> Element {
|
||||
rsx!(cx, "Hello {props.title}")
|
||||
}
|
||||
```
|
||||
|
||||
Global State
|
||||
```rust
|
||||
struct GlobalState { name: String }
|
||||
|
||||
fn App(cx: Context, props: &()) -> Element {
|
||||
use_provide_shared_state(cx, || GlobalState { name: String::from("Toby") })
|
||||
rsx!(cx, Leaf {})
|
||||
}
|
||||
|
||||
fn Leaf(cx: Context, props: &()) -> Element {
|
||||
let state = use_consume_shared_state::<GlobalState>(cx)?;
|
||||
rsx!(cx, "Hello {state.name}")
|
||||
}
|
||||
```
|
||||
|
||||
Router (inspired by Yew-Router)
|
||||
```rust
|
||||
#[derive(PartialEq, Clone, Hash, Eq, Routable)]
|
||||
enum Route {
|
||||
#[at("/")]
|
||||
Home,
|
||||
#[at("/post/{id}")]
|
||||
Post(id)
|
||||
}
|
||||
|
||||
fn App(cx: Context, props: &()) -> Element {
|
||||
let route = use_router(cx, Route::parse);
|
||||
cx.render(rsx!(div {
|
||||
{match route {
|
||||
Route::Home => rsx!( Home {} ),
|
||||
Route::Post(id) => rsx!( Post { id: id })
|
||||
}}
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
Suspense
|
||||
```rust
|
||||
fn App(cx: Context, props: &()) -> Element {
|
||||
let doggo = use_suspense(cx,
|
||||
|| async { reqwest::get("https://dog.ceo/api/breeds/image/random").await.unwrap().json::<Response>().await.unwrap() },
|
||||
|response| cx.render(rsx!( img { src: "{response.message}" }))
|
||||
);
|
||||
|
||||
cx.render(rsx!{
|
||||
div {
|
||||
"One doggo coming right up:"
|
||||
{doggo}
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
|
||||
+13
-10
@@ -5,7 +5,7 @@
|
||||
**Dioxus** is a framework and ecosystem for building fast, scalable, and robust user interfaces with the Rust programming language. This guide will help you get up-and-running with Dioxus running on the Web, Desktop, Mobile, and more.
|
||||
|
||||
```rust
|
||||
fn App((cx, props): Component<()>) -> Element {
|
||||
fn App(cx: Context, props: &()) -> Element {
|
||||
let mut count = use_state(cx, || 0);
|
||||
|
||||
cx.render(rsx!(
|
||||
@@ -16,11 +16,20 @@ fn App((cx, props): Component<()>) -> Element {
|
||||
};
|
||||
```
|
||||
|
||||
The Dioxus API and patterns closely resemble React - if this guide is lacking in any general concept or an error message is confusing, we recommend substituting "React" for "Dioxus" in your web search terms. A major goal of Dioxus is to provide a familiar toolkit for UI in Rust, so we've chosen to follow in the footsteps of popular UI frameworks (React, Redux, etc) - if you know React, then you already know Dioxus. If you don't know either, this guide will still help you!
|
||||
|
||||
In general, Dioxus and React share many functional similarities. If this guide is lacking in any general concept or an error message is confusing, React's documentation might be more helpful. We are dedicated to providing a *familiar* toolkit for UI in Rust, so we've chosen to follow in the footsteps of popular UI frameworks (React, Redux, etc). If you know React, then you already know Dioxus. If you don't know either, this guide will still help you!
|
||||
|
||||
> This is introduction book! For advanced topics, check out the [Reference Guide]() instead.
|
||||
|
||||
## Multiplatform
|
||||
|
||||
Dioxus is a *portable* toolkit, meaning the Core implementation can run anywhere with no platform-dependent linking. Unlike many other Rust frontend toolkits, Dioxus is not intrinsically linked to Web-Sys. In fact, every element and event listener can be swapped out at compile time. By default, Dioxus ships with the `Html` feature enabled which can be disabled depending on your target renderer.
|
||||
|
||||
Right now, we have several 1st-party renderers:
|
||||
- WebSys (for WASM)
|
||||
- Tao/Tokio (for Desktop apps)
|
||||
- Tao/Tokio (for Mobile apps)
|
||||
- SSR (for generating static markup)
|
||||
|
||||
### Web Support
|
||||
---
|
||||
|
||||
@@ -39,12 +48,7 @@ Examples:
|
||||
|
||||
### SSR Support
|
||||
---
|
||||
Dioxus supports server-side rendering! In a pinch, you can literally "debug" the VirtualDom:
|
||||
|
||||
```rust
|
||||
let dom = VirtualDom::new(App);
|
||||
println!("{:?}, dom");
|
||||
```
|
||||
Dioxus supports server-side rendering!
|
||||
|
||||
For rendering statically to an `.html` file or from a WebServer, then you'll want to make sure the `ssr` feature is enabled in the `dioxus` crate and use the `dioxus::ssr` API. We don't expect the SSR API to change drastically in the future.
|
||||
|
||||
@@ -52,7 +56,6 @@ For rendering statically to an `.html` file or from a WebServer, then you'll wan
|
||||
let contents = dioxus::ssr::render_vdom(&dom, |c| c);
|
||||
```
|
||||
|
||||
|
||||
[Jump to the getting started guide for SSR.]()
|
||||
|
||||
Examples:
|
||||
|
||||
@@ -26,7 +26,7 @@ container.push(green_light);
|
||||
container.push(yellow_light);
|
||||
container.push(red_light);
|
||||
|
||||
container.onclick(move |_| {
|
||||
container.set_onclick(move |_| {
|
||||
if red_light.enabled() {
|
||||
red_light.set_enabled(false);
|
||||
green_light.set_enabled(true);
|
||||
@@ -45,22 +45,23 @@ As the UI grows in scale, our logic to keep each element in the proper state wou
|
||||
Instead, with Dioxus, we *declare* what we want our UI to look like:
|
||||
|
||||
```rust
|
||||
let state = "red";
|
||||
let mut state = use_state(cx, || "red");
|
||||
|
||||
rsx!(
|
||||
cx.render(rsx!(
|
||||
Container {
|
||||
Light { color: "red", enabled: {state == "red"} }
|
||||
Light { color: "yellow", enabled: {state == "yellow"} }
|
||||
Light { color: "green", enabled: {state == "green"} }
|
||||
onclick: |_| {
|
||||
state = match state {
|
||||
Light { color: "red", enabled: format_args!("{}", state == "red") }
|
||||
Light { color: "yellow", enabled: format_args!("{}", state == "yellow") }
|
||||
Light { color: "green", enabled: format_args!("{}", state == "green") }
|
||||
|
||||
onclick: move |_| {
|
||||
state.set(match *state {
|
||||
"green" => "yellow",
|
||||
"yellow" => "red",
|
||||
"red" => "green",
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
))
|
||||
```
|
||||
|
||||
Remember: this concept is not new! Many frameworks are declarative - with React being the most popular. Declarative frameworks tend to be much more enjoyable to work with than imperative frameworks.
|
||||
|
||||
@@ -50,8 +50,8 @@ async fn ExampleLoader(cx: Context<()>) -> Vnode {
|
||||
This API stores the result on the Context object, so the loaded data is taken as reference.
|
||||
*/
|
||||
let name: &Result<SomeStructure> = use_fetch_data("http://example.com/json", ())
|
||||
.place_holder(|(cx, props)|rsx!{<div> "loading..." </div>})
|
||||
.delayed_place_holder(1000, |(cx, props)|rsx!{ <div> "still loading..." </div>})
|
||||
.place_holder(|cx, props|rsx!{<div> "loading..." </div>})
|
||||
.delayed_place_holder(1000, |cx, props|rsx!{ <div> "still loading..." </div>})
|
||||
.await;
|
||||
|
||||
match name {
|
||||
|
||||
@@ -21,9 +21,9 @@ fn test() -> DomTree {
|
||||
}
|
||||
}
|
||||
|
||||
static TestComponent: FC<()> = |(cx, props)|html!{<div>"Hello world"</div>};
|
||||
static TestComponent: FC<()> = |cx, props|html!{<div>"Hello world"</div>};
|
||||
|
||||
static TestComponent: FC<()> = |(cx, props)|{
|
||||
static TestComponent: FC<()> = |cx, props|{
|
||||
let g = "BLAH";
|
||||
html! {
|
||||
<div> "Hello world" </div>
|
||||
@@ -31,7 +31,7 @@ static TestComponent: FC<()> = |(cx, props)|{
|
||||
};
|
||||
|
||||
#[functional_component]
|
||||
static TestComponent: FC<{ name: String }> = |(cx, props)|html! { <div> "Hello {name}" </div> };
|
||||
static TestComponent: FC<{ name: String }> = |cx, props|html! { <div> "Hello {name}" </div> };
|
||||
```
|
||||
|
||||
## Why this behavior?
|
||||
|
||||
@@ -96,7 +96,7 @@ Sometimes you want a signal to propagate across your app, either through far-awa
|
||||
|
||||
```rust
|
||||
const TITLE: Atom<String> = || "".to_string();
|
||||
const Provider: FC<()> = |(cx, props)|{
|
||||
const Provider: FC<()> = |cx, props|{
|
||||
let title = use_signal(&cx, &TITLE);
|
||||
rsx!(cx, input { value: title })
|
||||
};
|
||||
@@ -105,7 +105,7 @@ const Provider: FC<()> = |(cx, props)|{
|
||||
If we use the `TITLE` atom in another component, we can cause updates to flow between components without calling render or diffing either component trees:
|
||||
|
||||
```rust
|
||||
const Receiver: FC<()> = |(cx, props)|{
|
||||
const Receiver: FC<()> = |cx, props|{
|
||||
let title = use_signal(&cx, &TITLE);
|
||||
log::info!("This will only be called once!");
|
||||
rsx!(cx,
|
||||
@@ -132,7 +132,7 @@ Dioxus automatically understands how to use your signals when mixed with iterato
|
||||
|
||||
```rust
|
||||
const DICT: AtomFamily<String, String> = |_| {};
|
||||
const List: FC<()> = |(cx, props)|{
|
||||
const List: FC<()> = |cx, props|{
|
||||
let dict = use_signal(&cx, &DICT);
|
||||
cx.render(rsx!(
|
||||
ul {
|
||||
|
||||
@@ -84,7 +84,7 @@ struct PostProps {
|
||||
|
||||
And our render function:
|
||||
```rust
|
||||
fn Post((cx, props): Scope<PostProps>) -> Element {
|
||||
fn Post(cx: Context, props: &PostProps) -> Element {
|
||||
cx.render(rsx!{
|
||||
div { class: "post-container"
|
||||
VoteButton {
|
||||
@@ -120,7 +120,7 @@ struct VoteButtonProps {
|
||||
score: i32
|
||||
}
|
||||
|
||||
fn VoteButton((cx, props): Scope<VoteButtonProps>) -> Element {
|
||||
fn VoteButton(cx: Context, props: &VoteButtonProps) -> Element {
|
||||
cx.render(rsx!{
|
||||
div { class: "votebutton"
|
||||
div { class: "arrow up" }
|
||||
@@ -145,7 +145,7 @@ struct TitleCardProps<'a> {
|
||||
title: &'a str,
|
||||
}
|
||||
|
||||
fn TitleCard<'a>((cx, props): Scope<'a, TitleCardProps>) -> Element<'a> {
|
||||
fn TitleCard(cx: Context, props: &TitleCardProps) -> Element {
|
||||
cx.render(rsx!{
|
||||
h1 { "{props.title}" }
|
||||
})
|
||||
@@ -174,8 +174,8 @@ function Component(props) {
|
||||
Because Dioxus needs to work with the rules of Rust it uses the `Context` object to maintain some internal bookkeeping. That's what the `Context` object is: a place for the component to store state, manage listeners, and allocate elements. Advanced users of Dioxus will want to learn how to properly leverage the `Context` object to build robust and performant extensions for Dioxus.
|
||||
|
||||
```rust
|
||||
fn Post((cx /* <-- our Context object*/, props): Scope<PostProps>) -> Element {
|
||||
cx.render(rsx!{ })
|
||||
fn Post(cx: Context, props: &PostProps) -> Element {
|
||||
cx.render(rsx!("hello"))
|
||||
}
|
||||
```
|
||||
## Moving forward
|
||||
|
||||
@@ -26,7 +26,7 @@ struct AppProps {
|
||||
Now that we have a "logged_in" flag accessible in our props, we can render two different screens:
|
||||
|
||||
```rust
|
||||
fn App((cx, props): Scope<AppProps>) -> Element {
|
||||
fn App(cx: Context, props: &AppProps) -> Element {
|
||||
if props.logged_in {
|
||||
cx.render(rsx!{
|
||||
DashboardScreen {}
|
||||
@@ -48,7 +48,7 @@ Rust provides us algebraic datatypes: enums that can contain values. Using the `
|
||||
For instance, we could run a function that returns a Result:
|
||||
|
||||
```rust
|
||||
fn App((cx, props): Scope<()>) -> Element {
|
||||
fn App(cx: Context, props: &())-> Element {
|
||||
match get_name() {
|
||||
Ok(name) => cx.render(rsx!( "Hello, {name}!" )),
|
||||
Err(err) => cx.render(rsx!( "Sorry, I don't know your name, because an error occurred: {err}" )),
|
||||
@@ -58,7 +58,7 @@ fn App((cx, props): Scope<()>) -> Element {
|
||||
|
||||
We can even match against values:
|
||||
```rust
|
||||
fn App((cx, props): Scope<()>) -> Element {
|
||||
fn App(cx: Context, props: &())-> Element {
|
||||
match get_name() {
|
||||
"jack" => cx.render(rsx!( "Hey Jack, how's Diane?" )),
|
||||
"diane" => cx.render(rsx!( "Hey Diana, how's Jack?" )),
|
||||
@@ -67,12 +67,12 @@ fn App((cx, props): Scope<()>) -> Element {
|
||||
}
|
||||
```
|
||||
|
||||
Do note: the `rsx!` macro returns a `Closure`, an anonymous function that has a unique type.
|
||||
Do note: the `rsx!` macro returns a `Closure`, an anonymous function that has a unique type. To turn our `rsx!` into Elements, we need to call `cx.render`.
|
||||
|
||||
To make patterns like these less verbose, the `rsx!` macro accepts an optional first argument on which it will call `render`. Our previous component can be shortened with this alternative syntax:
|
||||
|
||||
```rust
|
||||
fn App((cx, props): Scope<()>) -> Element {
|
||||
fn App(cx: Context, props: &())-> Element {
|
||||
match get_name() {
|
||||
"jack" => rsx!(cx, "Hey Jack, how's Diane?" ),
|
||||
"diane" => rsx!(cx, "Hey Diana, how's Jack?" ),
|
||||
@@ -83,13 +83,13 @@ fn App((cx, props): Scope<()>) -> Element {
|
||||
|
||||
This syntax even enables us to write a one-line component:
|
||||
```rust
|
||||
static App: Fc<()> = |(cx, props)| rsx!(cx, "hello world!");
|
||||
static App: FC<()> = |cx, props| rsx!(cx, "hello world!");
|
||||
```
|
||||
|
||||
Alternatively, for match statements, we can just return the builder itself and pass it into a final, single call to `cx.render`:
|
||||
|
||||
```rust
|
||||
fn App((cx, props): Scope<()>) -> Element {
|
||||
fn App(cx: Context, props: &())-> Element {
|
||||
let greeting = match get_name() {
|
||||
"jack" => rsx!("Hey Jack, how's Diane?" ),
|
||||
"diane" => rsx!("Hey Diana, how's Jack?" ),
|
||||
|
||||
@@ -35,7 +35,7 @@ fn main() {
|
||||
When Dioxus renders your app, it will pass an immutable reference of `PostProps` to your `Post` component. Here, you can pass the state down into children.
|
||||
|
||||
```rust
|
||||
fn App((cx, props): Scope<PostProps>) -> Element {
|
||||
fn App(cx: Context, props: &PostProps) -> Element {
|
||||
cx.render(rsx!{
|
||||
Title { title: &props.title }
|
||||
Score { score: &props.score }
|
||||
@@ -63,8 +63,8 @@ Instead, you'll want to store state internally in your components and let *that*
|
||||
The most common hook you'll use for storing state is `use_state`. `use_state` provides a slot for some data that allows you to read and update the value without accidentally mutating it.
|
||||
|
||||
```rust
|
||||
fn App((cx, props): Scope<()>) -> Element {
|
||||
let post = use_state(|| {
|
||||
fn App(cx: Context, props: &())-> Element {
|
||||
let post = use_state(cx, || {
|
||||
PostData {
|
||||
id: Uuid::new_v4(),
|
||||
score: 10,
|
||||
@@ -111,8 +111,8 @@ When responding to user-triggered events, we'll want to "listen" for an event on
|
||||
For example, let's say we provide a button to generate a new post. Whenever the user clicks the button, they get a new post. To achieve this functionality, we'll want to attach a function to the `on_click` method of `button`. Whenever the button is clicked, our function will run, and we'll get new Post data to work with.
|
||||
|
||||
```rust
|
||||
fn App((cx, props): Scope<()>) -> Element {
|
||||
let post = use_state(|| PostData::new());
|
||||
fn App(cx: Context, props: &())-> Element {
|
||||
let post = use_state(cx, || PostData::new());
|
||||
|
||||
cx.render(rsx!{
|
||||
button {
|
||||
@@ -134,8 +134,8 @@ We can use tasks in our components to build a tiny stopwatch that ticks every se
|
||||
|
||||
```rust
|
||||
|
||||
fn App((cx, props): Scope<()>) -> Element {
|
||||
let mut sec_elapsed = use_state(|| 0);
|
||||
fn App(cx: Context, props: &())-> Element {
|
||||
let mut sec_elapsed = use_state(cx, || 0);
|
||||
|
||||
cx.spawn_task(async move {
|
||||
TimeoutFuture::from_ms(1000).await;
|
||||
|
||||
@@ -74,7 +74,7 @@ struct PostListProps<'a> {
|
||||
Next, we're going to define our component:
|
||||
|
||||
```rust
|
||||
fn App((cx, props): Component<PostList>) -> Element {
|
||||
fn App(cx: Context, props: &PostList) -> Element {
|
||||
// First, we create a new iterator by mapping the post array
|
||||
let posts = props.posts.iter().map(|post| rsx!{
|
||||
Post {
|
||||
|
||||
@@ -1 +1,54 @@
|
||||
# Suspense
|
||||
|
||||
Suspense in Dioxus is enabled through placeholder nodes.
|
||||
|
||||
just a component that renders nodes into the placeholder after the future is finished?
|
||||
|
||||
in react, suspense is just completely pausing diffing while a
|
||||
|
||||
```rust
|
||||
let n = use_suspense(cx || {
|
||||
cx.render(rsx!{
|
||||
Suspense {
|
||||
prom: fut,
|
||||
callback: || {}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
suspense () {
|
||||
let value = use_state();
|
||||
if first_render {
|
||||
push_task({
|
||||
value.set(fut.await);
|
||||
});
|
||||
} else {
|
||||
callback(value)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let name = fetch_name().await;
|
||||
|
||||
|
||||
function ProfileDetails() {
|
||||
// Try to read user info, although it might not have loaded yet
|
||||
const user = resource.read();
|
||||
return <h1>{user.name}</h1>;
|
||||
}
|
||||
|
||||
|
||||
fn ProfileDteails() {
|
||||
let user = resource.suspend(cx, |l| rsx!("{l}"));
|
||||
|
||||
// waits for the resource to be ready and updates the placeholder with the tree
|
||||
let name = resource.suspend_with(cx, |val| rsx!( div { "hello" "{user}" } ));
|
||||
|
||||
cx.render(rsx!(
|
||||
div {
|
||||
{user}
|
||||
{name}
|
||||
}
|
||||
))
|
||||
}
|
||||
```
|
||||
@@ -107,7 +107,7 @@ To do this, we use the familiar struct-style syntax that Rust provides. Commas a
|
||||
```rust
|
||||
rsx!(
|
||||
div {
|
||||
hidden: true,
|
||||
hidden: "true",
|
||||
background_color: "blue",
|
||||
class: "card color-{mycolor}"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Hello World
|
||||
# Hello, World desktop app
|
||||
|
||||
Let's put together a simple "hello world" desktop application to get acquainted with Dioxus.
|
||||
|
||||
@@ -92,10 +92,10 @@ use dioxus::prelude::*;
|
||||
|
||||
|
||||
fn main() {
|
||||
dioxus::desktop::start(App, |c| c);
|
||||
dioxus::desktop::launch(App, |c| c);
|
||||
}
|
||||
|
||||
fn App((cx, props): Component<()>) -> Element {
|
||||
fn App(cx: Context, props: &()) -> Element {
|
||||
cx.render(rsx! (
|
||||
div { "Hello, world!" }
|
||||
))
|
||||
@@ -108,7 +108,7 @@ At this point, you could call `cargo run` and be greeted with a simple `Hello, W
|
||||
|
||||
### Dissecting our example
|
||||
|
||||
This bit of code imports everything from the the `prelude` module. This brings into scope the right traits, types, and macros needed for working with Dioxus.
|
||||
The `use` statement at the top of our app imports everything from the the `prelude` module. `use`-ing the prelude imports the right traits, types, and macros needed for working with Dioxus.
|
||||
|
||||
```rust
|
||||
use dioxus::prelude::*;
|
||||
@@ -118,44 +118,34 @@ This initialization code launches a Tokio runtime on a helper thread where your
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
dioxus::desktop::start(App, |c| c);
|
||||
dioxus::desktop::launch(App, |c| c);
|
||||
}
|
||||
```
|
||||
|
||||
Finally, our app. Every component in Dioxus is a function that takes in `Context` and `Props` and returns an `Element`.
|
||||
|
||||
```rust
|
||||
fn App((cx, props): Component<()>) -> Element {
|
||||
fn App(cx: Context, props: &()) -> Element {
|
||||
cx.render(rsx! {
|
||||
div { "Hello, world!" }
|
||||
})
|
||||
}
|
||||
```
|
||||
In cases where props need to borrow from their parent, you will need to specify lifetimes using the function syntax:
|
||||
|
||||
Writing `fn App(cx: Context, props: &()) -> Element {` might become tedious. Rust will also let you write functions as static closures, but these types of Components cannot have props that borrow data.
|
||||
|
||||
```rust
|
||||
fn App<'a>(cx: Component<'a, ()>) -> Element<'a> {
|
||||
cx.render(rsx! {
|
||||
div { "Hello, world!" }
|
||||
})
|
||||
}
|
||||
static App: FC<()> = |cx, props| cx.render(rsx!(div { "Hello, world!" }));
|
||||
```
|
||||
|
||||
Writing `fn App((cx, props): Component<()>) -> Element {` might become tedious. Rust will also let you write functions as static closures, but these types of Components cannot have props that borrow data.
|
||||
```rust
|
||||
static App: Fc<()> = |(cx, props)| {
|
||||
cx.render(rsx! {
|
||||
div { "Hello, world!" }
|
||||
})
|
||||
};
|
||||
```
|
||||
### What is this `Context` object?
|
||||
|
||||
### The `Context` object
|
||||
Coming from React, the `Context` object might be confusing. In React, you'll want to store data between renders with hooks. However, hooks rely on global variables which make them difficult to integrate in multi-tenant systems like server-rendering.
|
||||
|
||||
In React, you'll want to store data between renders with hooks. However, hooks rely on global variables which make them difficult to integrate in multi-tenant systems like server-rendering. In Dioxus, you are given an explicit `Context` object to control how the component renders and stores data.
|
||||
In Dioxus, you are given an explicit `Context` object to control how the component renders and stores data. The `Context` object provides a handful of useful APIs for features like suspense, rendering, and more.
|
||||
|
||||
### The `rsx!` macro
|
||||
## Moving on
|
||||
|
||||
Congrats! You've built your first desktop application with Dioxus. Next, we're going to learn about the basics of building interactive user interfaces.
|
||||
|
||||
Next, we're greeted with the `rsx!` macro. This lets us add a custom DSL for declaratively building the structure of our app. The semantics of this macro are similar to that of JSX and HTML, though with a familiar Rust-y interface. The `html!` macro is also available for writing components with a JSX/HTML syntax.
|
||||
|
||||
The `rsx!` macro is lazy: it does not immediately produce elements or allocates, but rather builds a closure which can be rendered with `cx.render`.
|
||||
@@ -20,6 +20,16 @@ Dioxus requires a few main things to get up and running:
|
||||
|
||||
Dioxus integrates very well with the Rust-Analyzer IDE plugin which will provide appropriate syntax highlighting, code navigation, folding, and more.
|
||||
|
||||
### Installing Rust
|
||||
|
||||
Head over to [https://rust-lang.org](http://rust-lang.org) and install the Rust compiler.
|
||||
|
||||
Once installed, make sure to install wasm32-unknown-unknown as a target if you're planning on deploying your app to the web.
|
||||
|
||||
```
|
||||
rustup target add wasm32-unknown-uknown
|
||||
```
|
||||
|
||||
### Dioxus-CLI for dev server, bundling, etc.
|
||||
|
||||
We also recommend installing the Dioxus CLI. The Dioxus CLI automates building and packaging for various targets and integrates with simulators, development servers, and app deployment. To install the CLI, you'll need cargo (should be automatically installed with Rust):
|
||||
|
||||
@@ -48,3 +48,148 @@ These web-specific examples must be run with `dioxus-cli` using `dioxus develop
|
||||
| ------- | ------------ |
|
||||
| asd | this does |
|
||||
| asd | this does |
|
||||
|
||||
|
||||
|
||||
## Show me some examples!
|
||||
|
||||
In our collection of examples, guides, and tutorials, we have:
|
||||
- The book (an introductory course)
|
||||
- The guide (an in-depth analysis of everything in Dioxus)
|
||||
- The reference (a collection of examples with heavy documentation)
|
||||
- The general examples
|
||||
- The platform-specific examples (web, ssr, desktop, mobile, server)
|
||||
|
||||
Here's what a few common tasks look like in Dioxus:
|
||||
|
||||
Nested components with children and internal state:
|
||||
```rust
|
||||
fn App(cx: Context, props: &()) -> Element {
|
||||
cx.render(rsx!( Toggle { "Toggle me" } ))
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Props)]
|
||||
struct ToggleProps { children: Element }
|
||||
|
||||
fn Toggle(cx: Context, props: &ToggleProps) -> Element {
|
||||
let mut toggled = use_state(cx, || false);
|
||||
cx.render(rsx!{
|
||||
div {
|
||||
{&props.children}
|
||||
button { onclick: move |_| toggled.set(true),
|
||||
{toggled.and_then(|| "On").or_else(|| "Off")}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Controlled inputs:
|
||||
```rust
|
||||
fn App(cx: Context, props: &()) -> Element {
|
||||
let value = use_state(cx, String::new);
|
||||
cx.render(rsx!(
|
||||
input {
|
||||
"type": "text",
|
||||
value: "{value}",
|
||||
oninput: move |evt| value.set(evt.value.clone())
|
||||
}
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
Lists and Conditional rendering:
|
||||
```rust
|
||||
fn App(cx: Context, props: &()) -> Element {
|
||||
let list = (0..10).map(|i| {
|
||||
rsx!(li { key: "{i}", "Value: {i}" })
|
||||
});
|
||||
|
||||
let title = match list.len() {
|
||||
0 => rsx!("Not enough"),
|
||||
_ => rsx!("Plenty!"),
|
||||
};
|
||||
|
||||
if should_show {
|
||||
cx.render(rsx!(
|
||||
{title}
|
||||
ul { {list} }
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Tiny components:
|
||||
```rust
|
||||
static App: FC<()> = |cx, _| rsx!(cx, div {"hello world!"});
|
||||
```
|
||||
|
||||
Borrowed prop contents:
|
||||
```rust
|
||||
fn App(cx: Context, props: &()) -> Element {
|
||||
let name = use_state(cx, || String::from("example"));
|
||||
rsx!(cx, Child { title: name.as_str() })
|
||||
}
|
||||
|
||||
#[derive(Props)]
|
||||
struct ChildProps<'a> { title: &'a str }
|
||||
|
||||
fn Child(cx: Context, props: &ChildProps) -> Element {
|
||||
rsx!(cx, "Hello {props.title}")
|
||||
}
|
||||
```
|
||||
|
||||
Global State
|
||||
```rust
|
||||
struct GlobalState { name: String }
|
||||
|
||||
fn App(cx: Context, props: &()) -> Element {
|
||||
use_provide_shared_state(cx, || GlobalState { name: String::from("Toby") })
|
||||
rsx!(cx, Leaf {})
|
||||
}
|
||||
|
||||
fn Leaf(cx: Context, props: &()) -> Element {
|
||||
let state = use_consume_shared_state::<GlobalState>(cx)?;
|
||||
rsx!(cx, "Hello {state.name}")
|
||||
}
|
||||
```
|
||||
|
||||
Router (inspired by Yew-Router)
|
||||
```rust
|
||||
#[derive(PartialEq, Clone, Hash, Eq, Routable)]
|
||||
enum Route {
|
||||
#[at("/")]
|
||||
Home,
|
||||
#[at("/post/{id}")]
|
||||
Post(id)
|
||||
}
|
||||
|
||||
fn App(cx: Context, props: &()) -> Element {
|
||||
let route = use_router(cx, Route::parse);
|
||||
cx.render(rsx!(div {
|
||||
{match route {
|
||||
Route::Home => rsx!( Home {} ),
|
||||
Route::Post(id) => rsx!( Post { id: id })
|
||||
}}
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
Suspense
|
||||
```rust
|
||||
fn App(cx: Context, props: &()) -> Element {
|
||||
let doggo = use_suspense(cx,
|
||||
|| async { reqwest::get("https://dog.ceo/api/breeds/image/random").await.unwrap().json::<Response>().await.unwrap() },
|
||||
|response| cx.render(rsx!( img { src: "{response.message}" }))
|
||||
);
|
||||
|
||||
cx.render(rsx!{
|
||||
div {
|
||||
"One doggo coming right up:"
|
||||
{doggo}
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
+1
-1
@@ -11,7 +11,7 @@ async fn main() {
|
||||
dioxus::desktop::launch(App, |c| c);
|
||||
}
|
||||
|
||||
pub static App: FC<()> = |(cx, _)| {
|
||||
pub static App: FC<()> = |cx, _| {
|
||||
let count = use_state(cx, || 0);
|
||||
let mut direction = use_state(cx, || 1);
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ fn main() {
|
||||
dioxus::desktop::launch(App, |c| c);
|
||||
}
|
||||
|
||||
fn App((cx, props): Scope<()>) -> Element {
|
||||
fn App(cx: Context, props: &()) -> Element {
|
||||
let text: &mut Vec<String> = cx.use_hook(|_| vec![String::from("abc=def")], |f| f);
|
||||
|
||||
let first = text.get_mut(0).unwrap();
|
||||
@@ -39,11 +39,7 @@ struct C1Props<'a> {
|
||||
text: &'a mut String,
|
||||
}
|
||||
|
||||
impl<'a> Drop for C1Props<'a> {
|
||||
fn drop(&mut self) {}
|
||||
}
|
||||
|
||||
fn Child1<'a>((cx, props): Scope<'a, C1Props>) -> Element<'a> {
|
||||
fn Child1(cx: Context, props: &C1Props) -> Element {
|
||||
let (left, right) = props.text.split_once("=").unwrap();
|
||||
|
||||
cx.render(rsx! {
|
||||
@@ -59,7 +55,7 @@ struct C2Props<'a> {
|
||||
text: &'a str,
|
||||
}
|
||||
|
||||
fn Child2<'a>((cx, props): Scope<'a, C2Props>) -> Element<'a> {
|
||||
fn Child2(cx: Context, props: &C2Props) -> Element {
|
||||
cx.render(rsx! {
|
||||
Child3 {
|
||||
text: props.text
|
||||
@@ -72,7 +68,7 @@ struct C3Props<'a> {
|
||||
text: &'a str,
|
||||
}
|
||||
|
||||
fn Child3<'a>((cx, props): Scope<'a, C3Props>) -> Element<'a> {
|
||||
fn Child3(cx: Context, props: &C3Props) -> Element {
|
||||
cx.render(rsx! {
|
||||
div { "{props.text}"}
|
||||
})
|
||||
|
||||
+79
-76
@@ -3,97 +3,82 @@ This example is a simple iOS-style calculator. This particular example can run a
|
||||
This calculator version uses React-style state management. All state is held as individual use_states.
|
||||
*/
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use dioxus::events::*;
|
||||
use dioxus::prelude::*;
|
||||
use separator::Separatable;
|
||||
|
||||
fn main() {
|
||||
dioxus::desktop::launch(APP, |cfg| cfg);
|
||||
}
|
||||
|
||||
const APP: FC<()> = |(cx, _)| {
|
||||
const APP: FC<()> = |cx, _| {
|
||||
let cur_val = use_state(cx, || 0.0_f64);
|
||||
let operator = use_state(cx, || None as Option<&'static str>);
|
||||
let display_value = use_state(cx, || String::from(""));
|
||||
|
||||
let clear_display = display_value == "0";
|
||||
let clear_text = if clear_display { "C" } else { "AC" };
|
||||
|
||||
let toggle_percent = move |_| todo!();
|
||||
let input_digit = move |num: u8| display_value.modify().push_str(num.to_string().as_str());
|
||||
|
||||
let input_dot = move || display_value.modify().push_str(".");
|
||||
|
||||
let perform_operation = move || {
|
||||
if let Some(op) = operator.as_ref() {
|
||||
let rhs = display_value.parse::<f64>().unwrap();
|
||||
let new_val = match *op {
|
||||
"+" => *cur_val + rhs,
|
||||
"-" => *cur_val - rhs,
|
||||
"*" => *cur_val * rhs,
|
||||
"/" => *cur_val / rhs,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
cur_val.set(new_val);
|
||||
display_value.set(new_val.to_string());
|
||||
operator.set(None);
|
||||
}
|
||||
};
|
||||
|
||||
let toggle_sign = move |_| {
|
||||
if display_value.starts_with("-") {
|
||||
display_value.set(display_value.trim_start_matches("-").to_string())
|
||||
} else {
|
||||
display_value.set(format!("-{}", *display_value))
|
||||
}
|
||||
};
|
||||
|
||||
let toggle_percent = move |_| todo!();
|
||||
|
||||
let clear_key = move |_| {
|
||||
display_value.set("0".to_string());
|
||||
if !clear_display {
|
||||
operator.set(None);
|
||||
cur_val.set(0.0);
|
||||
}
|
||||
};
|
||||
|
||||
let keydownhandler = move |evt: KeyboardEvent| match evt.key_code {
|
||||
KeyCode::Add => operator.set(Some("+")),
|
||||
KeyCode::Subtract => operator.set(Some("-")),
|
||||
KeyCode::Divide => operator.set(Some("/")),
|
||||
KeyCode::Multiply => operator.set(Some("*")),
|
||||
KeyCode::Num0 => input_digit(0),
|
||||
KeyCode::Num1 => input_digit(1),
|
||||
KeyCode::Num2 => input_digit(2),
|
||||
KeyCode::Num3 => input_digit(3),
|
||||
KeyCode::Num4 => input_digit(4),
|
||||
KeyCode::Num5 => input_digit(5),
|
||||
KeyCode::Num6 => input_digit(6),
|
||||
KeyCode::Num7 => input_digit(7),
|
||||
KeyCode::Num8 => input_digit(8),
|
||||
KeyCode::Num9 => input_digit(9),
|
||||
KeyCode::Backspace => {
|
||||
if !display_value.as_str().eq("0") {
|
||||
display_value.modify().pop();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
use separator::Separatable;
|
||||
let formatted_display = cur_val.separated_string();
|
||||
|
||||
rsx!(cx, div {
|
||||
class: "calculator", onkeydown: {keydownhandler}
|
||||
div { class: "calculator-display", "{formatted_display}" }
|
||||
class: "calculator",
|
||||
onkeydown: move |evt| match evt.key_code {
|
||||
KeyCode::Add => operator.set(Some("+")),
|
||||
KeyCode::Subtract => operator.set(Some("-")),
|
||||
KeyCode::Divide => operator.set(Some("/")),
|
||||
KeyCode::Multiply => operator.set(Some("*")),
|
||||
KeyCode::Num0 => input_digit(0),
|
||||
KeyCode::Num1 => input_digit(1),
|
||||
KeyCode::Num2 => input_digit(2),
|
||||
KeyCode::Num3 => input_digit(3),
|
||||
KeyCode::Num4 => input_digit(4),
|
||||
KeyCode::Num5 => input_digit(5),
|
||||
KeyCode::Num6 => input_digit(6),
|
||||
KeyCode::Num7 => input_digit(7),
|
||||
KeyCode::Num8 => input_digit(8),
|
||||
KeyCode::Num9 => input_digit(9),
|
||||
KeyCode::Backspace => {
|
||||
if !display_value.as_str().eq("0") {
|
||||
display_value.modify().pop();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
div { class: "calculator-display", {[format_args!("{}", cur_val.separated_string())]} }
|
||||
div { class: "input-keys"
|
||||
div { class: "function-keys"
|
||||
CalculatorKey { name: "key-clear", onclick: {clear_key} "{clear_text}" }
|
||||
CalculatorKey { name: "key-sign", onclick: {toggle_sign}, "±"}
|
||||
CalculatorKey { name: "key-percent", onclick: {toggle_percent} "%"}
|
||||
CalculatorKey {
|
||||
{[if display_value == "0" { "C" } else { "AC" }]}
|
||||
name: "key-clear",
|
||||
onclick: move |_| {
|
||||
display_value.set("0".to_string());
|
||||
if display_value != "0" {
|
||||
operator.set(None);
|
||||
cur_val.set(0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
CalculatorKey {
|
||||
"±"
|
||||
name: "key-sign",
|
||||
onclick: move |_| {
|
||||
if display_value.starts_with("-") {
|
||||
display_value.set(display_value.trim_start_matches("-").to_string())
|
||||
} else {
|
||||
display_value.set(format!("-{}", *display_value))
|
||||
}
|
||||
},
|
||||
}
|
||||
CalculatorKey {
|
||||
"%"
|
||||
onclick: {toggle_percent}
|
||||
name: "key-percent",
|
||||
}
|
||||
}
|
||||
div { class: "digit-keys"
|
||||
CalculatorKey { name: "key-0", onclick: move |_| input_digit(0), "0" }
|
||||
CalculatorKey { name: "key-dot", onclick: move |_| input_dot(), "●" }
|
||||
CalculatorKey { name: "key-dot", onclick: move |_| display_value.modify().push_str("."), "●" }
|
||||
|
||||
{(1..9).map(|k| rsx!{
|
||||
CalculatorKey { key: "{k}", name: "key-{k}", onclick: move |_| input_digit(k), "{k}" }
|
||||
@@ -104,7 +89,25 @@ const APP: FC<()> = |(cx, _)| {
|
||||
CalculatorKey { name: "key-multiply", onclick: move |_| operator.set(Some("*")) "×" }
|
||||
CalculatorKey { name: "key-subtract", onclick: move |_| operator.set(Some("-")) "−" }
|
||||
CalculatorKey { name: "key-add", onclick: move |_| operator.set(Some("+")) "+" }
|
||||
CalculatorKey { name: "key-equals", onclick: move |_| perform_operation() "=" }
|
||||
CalculatorKey {
|
||||
"="
|
||||
name: "key-equals",
|
||||
onclick: move |_| {
|
||||
if let Some(op) = operator.as_ref() {
|
||||
let rhs = display_value.parse::<f64>().unwrap();
|
||||
let new_val = match *op {
|
||||
"+" => *cur_val + rhs,
|
||||
"-" => *cur_val - rhs,
|
||||
"*" => *cur_val * rhs,
|
||||
"/" => *cur_val / rhs,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
cur_val.set(new_val);
|
||||
display_value.set(new_val.to_string());
|
||||
operator.set(None);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -113,11 +116,11 @@ const APP: FC<()> = |(cx, _)| {
|
||||
#[derive(Props)]
|
||||
struct CalculatorKeyProps<'a> {
|
||||
name: &'static str,
|
||||
onclick: &'a dyn Fn(MouseEvent),
|
||||
children: ScopeChildren<'a>,
|
||||
onclick: &'a dyn Fn(Arc<MouseEvent>),
|
||||
children: Element,
|
||||
}
|
||||
|
||||
fn CalculatorKey<'a>((cx, props): Scope<'a, CalculatorKeyProps<'a>>) -> Element {
|
||||
fn CalculatorKey<'a>(cx: Context, props: &CalculatorKeyProps) -> Element {
|
||||
rsx!(cx, button {
|
||||
class: "calculator-key {props.name}"
|
||||
onclick: {props.onclick}
|
||||
|
||||
@@ -9,7 +9,7 @@ fn main() {
|
||||
println!("{}", dom);
|
||||
}
|
||||
|
||||
pub static EXAMPLE: FC<()> = |(cx, _)| {
|
||||
pub static EXAMPLE: FC<()> = |cx, _| {
|
||||
let list = (0..10).map(|_f| {
|
||||
rsx! {
|
||||
"{_f}"
|
||||
|
||||
@@ -31,7 +31,7 @@ fn html_usage() {
|
||||
// let p = rsx!(div { {f} });
|
||||
}
|
||||
|
||||
static App2: FC<()> = |(cx, _)| cx.render(rsx!("hello world!"));
|
||||
static App2: FC<()> = |cx, _| cx.render(rsx!("hello world!"));
|
||||
|
||||
static App: FC<()> = |cx, props| {
|
||||
let name = cx.use_state(|| 0);
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ pub struct Client {
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
static App: FC<()> = |(cx, _)| {
|
||||
static App: FC<()> = |cx, _| {
|
||||
let mut clients = use_ref(cx, || vec![] as Vec<Client>);
|
||||
let mut scene = use_state(cx, || Scene::ClientsList);
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ pub struct Client {
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
static App: FC<()> = |(cx, _)| {
|
||||
static App: FC<()> = |cx, _| {
|
||||
let mut scene = use_state(cx, || Scene::ClientsList);
|
||||
let clients = use_ref(cx, || vec![] as Vec<Client>);
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ pub struct TodoItem {
|
||||
}
|
||||
pub type Todos = HashMap<u32, TodoItem>;
|
||||
|
||||
pub static App: FC<()> = |(cx, _)| {
|
||||
pub static App: FC<()> = |cx, _| {
|
||||
// Share our TodoList to the todos themselves
|
||||
use_provide_state(cx, Todos::new);
|
||||
|
||||
|
||||
@@ -1,50 +1,38 @@
|
||||
use dioxus::{events::MouseEvent, prelude::*};
|
||||
use fxhash::FxBuildHasher;
|
||||
use std::rc::Rc;
|
||||
use dioxus::prelude::*;
|
||||
use rand::prelude::*;
|
||||
|
||||
fn main() {
|
||||
dioxus::desktop::launch(App, |c| c);
|
||||
dioxus::web::launch(App, |c| c);
|
||||
// dioxus::desktop::launch(App, |c| c);
|
||||
}
|
||||
|
||||
// We use a special immutable hashmap to make hashmap operations efficient
|
||||
type RowList = im_rc::HashMap<usize, Rc<str>, FxBuildHasher>;
|
||||
#[derive(Clone, PartialEq)]
|
||||
struct Label {
|
||||
key: usize,
|
||||
labels: [&'static str; 3],
|
||||
}
|
||||
|
||||
static App: FC<()> = |(cx, _props)| {
|
||||
let mut items = use_state(cx, || RowList::default());
|
||||
|
||||
let create_rendered_rows = move |from, num| move |_| items.set(create_row_list(from, num));
|
||||
|
||||
let mut append_1_000_rows =
|
||||
move |_| items.set(create_row_list(items.len(), 1000).union((*items).clone()));
|
||||
|
||||
let update_every_10th_row = move |_| {
|
||||
let mut new_items = (*items).clone();
|
||||
let mut small_rng = SmallRng::from_entropy();
|
||||
new_items.iter_mut().step_by(10).for_each(|(_, val)| {
|
||||
*val = create_new_row_label(&mut String::with_capacity(30), &mut small_rng)
|
||||
});
|
||||
items.set(new_items);
|
||||
};
|
||||
let clear_rows = move |_| items.set(RowList::default());
|
||||
|
||||
let swap_rows = move |_| {
|
||||
// this looks a bit ugly because we're using a hashmap instead of a vec
|
||||
if items.len() > 998 {
|
||||
let mut new_items = (*items).clone();
|
||||
let a = new_items.get(&0).unwrap().clone();
|
||||
*new_items.get_mut(&0).unwrap() = new_items.get(&998).unwrap().clone();
|
||||
*new_items.get_mut(&998).unwrap() = a;
|
||||
items.set(new_items);
|
||||
impl Label {
|
||||
fn new_list(num: usize) -> Vec<Self> {
|
||||
let mut rng = SmallRng::from_entropy();
|
||||
let mut labels = Vec::with_capacity(num);
|
||||
for _ in 0..num {
|
||||
labels.push(Label {
|
||||
key: 0,
|
||||
labels: [
|
||||
ADJECTIVES.choose(&mut rng).unwrap(),
|
||||
COLOURS.choose(&mut rng).unwrap(),
|
||||
NOUNS.choose(&mut rng).unwrap(),
|
||||
],
|
||||
});
|
||||
}
|
||||
};
|
||||
labels
|
||||
}
|
||||
}
|
||||
|
||||
let rows = items.iter().map(|(key, value)| {
|
||||
rsx!(Row {
|
||||
key: "{key}",
|
||||
row_id: *key as usize,
|
||||
label: value.clone(),
|
||||
})
|
||||
});
|
||||
static App: FC<()> = |cx, _props| {
|
||||
let mut items = use_ref(cx, || vec![]);
|
||||
let mut selected = use_state(cx, || None);
|
||||
|
||||
cx.render(rsx! {
|
||||
div { class: "container"
|
||||
@@ -53,22 +41,49 @@ static App: FC<()> = |(cx, _props)| {
|
||||
div { class: "col-md-6", h1 { "Dioxus" } }
|
||||
div { class: "col-md-6"
|
||||
div { class: "row"
|
||||
ActionButton { name: "Create 1,000 rows", id: "run", onclick: {create_rendered_rows(0, 1_000)} }
|
||||
ActionButton { name: "Create 10,000 rows", id: "runlots", onclick: {create_rendered_rows(0, 10_000)} }
|
||||
ActionButton { name: "Append 1,000 rows", id: "add", onclick: {append_1_000_rows} }
|
||||
ActionButton { name: "Update every 10th row", id: "update", onclick: {update_every_10th_row} }
|
||||
ActionButton { name: "Clear", id: "clear", onclick: {clear_rows} }
|
||||
ActionButton { name: "Swap rows", id: "swaprows", onclick: {swap_rows} }
|
||||
ActionButton { name: "Create 1,000 rows", id: "run",
|
||||
onclick: move || items.set(Label::new_list(1_000)),
|
||||
}
|
||||
ActionButton { name: "Create 10,000 rows", id: "runlots",
|
||||
onclick: move || items.set(Label::new_list(10_000)),
|
||||
}
|
||||
ActionButton { name: "Append 1,000 rows", id: "add",
|
||||
onclick: move || items.write().extend(Label::new_list(1_000)),
|
||||
}
|
||||
ActionButton { name: "Update every 10th row", id: "update",
|
||||
onclick: move || items.write().iter_mut().step_by(10).for_each(|item| item.labels[2] = "!!!"),
|
||||
}
|
||||
ActionButton { name: "Clear", id: "clear",
|
||||
onclick: move || items.write().clear(),
|
||||
}
|
||||
ActionButton { name: "Swap rows", id: "swaprows",
|
||||
onclick: move || items.write().swap(0, 998),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
table {
|
||||
tbody {
|
||||
{rows}
|
||||
{items.read().iter().enumerate().map(|(id, item)| {
|
||||
let is_in_danger = if (*selected).map(|s| s == id).unwrap_or(false) {"danger"} else {""};
|
||||
rsx!(tr { class: "{is_in_danger}"
|
||||
td { class:"col-md-1" }
|
||||
td { class:"col-md-1", "{item.key}" }
|
||||
td { class:"col-md-1", onclick: move |_| selected.set(Some(id)),
|
||||
a { class: "lbl", {item.labels} }
|
||||
}
|
||||
td { class: "col-md-1"
|
||||
a { class: "remove", onclick: move |_| { items.write().remove(id); },
|
||||
span { class: "glyphicon glyphicon-remove remove" aria_hidden: "true" }
|
||||
}
|
||||
}
|
||||
td { class: "col-md-6" }
|
||||
})
|
||||
})}
|
||||
}
|
||||
}
|
||||
span {}
|
||||
// span { class: "preloadicon glyphicon glyphicon-remove" aria_hidden: "true" }
|
||||
}
|
||||
})
|
||||
};
|
||||
@@ -77,59 +92,17 @@ static App: FC<()> = |(cx, _props)| {
|
||||
struct ActionButtonProps<'a> {
|
||||
name: &'static str,
|
||||
id: &'static str,
|
||||
onclick: &'a dyn Fn(MouseEvent),
|
||||
onclick: &'a dyn Fn(),
|
||||
}
|
||||
|
||||
fn ActionButton<'a>((cx, props): Scope<'a, ActionButtonProps>) -> Element<'a> {
|
||||
fn ActionButton(cx: Context, props: &ActionButtonProps) -> Element {
|
||||
rsx!(cx, div { class: "col-sm-6 smallpad"
|
||||
button { class:"btn btn-primary btn-block", r#type: "button", id: "{props.id}", onclick: {props.onclick},
|
||||
button { class:"btn btn-primary btn-block", r#type: "button", id: "{props.id}", onclick: move |_| (props.onclick)(),
|
||||
"{props.name}"
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Props)]
|
||||
struct RowProps {
|
||||
row_id: usize,
|
||||
label: Rc<str>,
|
||||
}
|
||||
fn Row((cx, props): Scope<RowProps>) -> Element {
|
||||
rsx!(cx, tr {
|
||||
td { class:"col-md-1", "{props.row_id}" }
|
||||
td { class:"col-md-1", onclick: move |_| { /* run onselect */ }
|
||||
a { class: "lbl", "{props.label}" }
|
||||
}
|
||||
td { class: "col-md-1"
|
||||
a { class: "remove", onclick: move |_| {/* remove */}
|
||||
span { class: "glyphicon glyphicon-remove remove" aria_hidden: "true" }
|
||||
}
|
||||
}
|
||||
td { class: "col-md-6" }
|
||||
})
|
||||
}
|
||||
|
||||
use rand::prelude::*;
|
||||
fn create_new_row_label(label: &mut String, rng: &mut SmallRng) -> Rc<str> {
|
||||
label.push_str(ADJECTIVES.choose(rng).unwrap());
|
||||
label.push(' ');
|
||||
label.push_str(COLOURS.choose(rng).unwrap());
|
||||
label.push(' ');
|
||||
label.push_str(NOUNS.choose(rng).unwrap());
|
||||
Rc::from(label.as_ref())
|
||||
}
|
||||
|
||||
fn create_row_list(from: usize, num: usize) -> RowList {
|
||||
let mut small_rng = SmallRng::from_entropy();
|
||||
let mut buf = String::with_capacity(35);
|
||||
(from..num + from)
|
||||
.map(|f| {
|
||||
let o = (f, create_new_row_label(&mut buf, &mut small_rng));
|
||||
buf.clear();
|
||||
o
|
||||
})
|
||||
.collect::<RowList>()
|
||||
}
|
||||
|
||||
static ADJECTIVES: &[&str] = &[
|
||||
"pretty",
|
||||
"large",
|
||||
@@ -167,3 +140,24 @@ static NOUNS: &[&str] = &[
|
||||
"table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger",
|
||||
"pizza", "mouse", "keyboard",
|
||||
];
|
||||
|
||||
// #[derive(PartialEq, Props)]
|
||||
// struct RowProps<'a> {
|
||||
// row_id: usize,
|
||||
// label: &'a Label,
|
||||
// }
|
||||
|
||||
// fn Row(cx: Context, props: &RowProps) -> Element {
|
||||
// rsx!(cx, tr {
|
||||
// td { class:"col-md-1", "{props.row_id}" }
|
||||
// td { class:"col-md-1", onclick: move |_| { /* run onselect */ }
|
||||
// a { class: "lbl", {props.label.labels} }
|
||||
// }
|
||||
// td { class: "col-md-1"
|
||||
// a { class: "remove", onclick: move |_| {/* remove */}
|
||||
// span { class: "glyphicon glyphicon-remove remove" aria_hidden: "true" }
|
||||
// }
|
||||
// }
|
||||
// td { class: "col-md-6" }
|
||||
// })
|
||||
// }
|
||||
@@ -11,7 +11,7 @@ fn main() {
|
||||
dioxus::desktop::launch(App, |c| c);
|
||||
}
|
||||
|
||||
pub static App: FC<()> = |(cx, _)| {
|
||||
pub static App: FC<()> = |cx, _| {
|
||||
let state = use_state(cx, PlayerState::new);
|
||||
|
||||
let is_playing = state.is_playing();
|
||||
|
||||
@@ -14,7 +14,7 @@ fn main() {
|
||||
dioxus_web::launch(APP, |c| c)
|
||||
}
|
||||
|
||||
static APP: FC<()> = |(cx, _)| {
|
||||
static APP: FC<()> = |cx, _| {
|
||||
let mut count = use_state(cx, || 3);
|
||||
let content = use_state(cx, || String::from("h1"));
|
||||
let text_content = use_state(cx, || String::from("Hello, world!"));
|
||||
@@ -86,4 +86,4 @@ fn render_list(cx: Context, count: usize) -> Element {
|
||||
rsx!(cx, ul { {items} })
|
||||
}
|
||||
|
||||
static CHILD: FC<()> = |(cx, _)| rsx!(cx, div {"hello child"});
|
||||
static CHILD: FC<()> = |cx, _| rsx!(cx, div {"hello child"});
|
||||
@@ -28,7 +28,7 @@ pub struct Client {
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
static App: FC<()> = |(cx, _)| {
|
||||
static App: FC<()> = |cx, _| {
|
||||
let scene = use_state(cx, || Scene::ClientsList);
|
||||
let clients = use_ref(cx, || vec![] as Vec<Client>);
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
|
||||
### Aren't VDOMs just pure overhead? Why not something like Solid or Svelte?
|
||||
Remember: Dioxus is a library - not a compiler like Svelte. Plus, the inner VirtualDOM allows Dioxus to easily port into different runtimes, support SSR, and run remotely in the cloud. VDOMs tend to more ergonomic to work with and feel roughly like natural Rust code. The overhead of Dioxus is **extraordinarily** minimal... sure, there may be some overhead but on an order of magnitude lower than the time required to actually update the page.
|
||||
|
||||
|
||||
### Isn't the overhead for interacting with the DOM from Wasm too much?
|
||||
The overhead layer between Wasm and JS APIs is extremely poorly understood. Rust web benchmarks typically suffer from differences in how Rust and JS cache strings. In Dioxus, we solve most of these issues and our JS Framework Benchmark actually beats the Wasm Bindgen benchmark in many cases. Compared to a "pure vanilla JS" solution, Dioxus adds less than 5% of overhead and takes advantage of batched DOM manipulation.
|
||||
|
||||
### Aren't Wasm binaries too huge to deploy in production?
|
||||
Wasm binary sizes are another poorly understood characteristic of Rust web apps. 50kb of Wasm and 50kb of JS are _not_ made equally. In JS, the code must be downloaded _first_ and _then_ JIT-ted. Just-in-time compiling 50kb of JavaScript takes a while which is why 50kb of JavaScript sounds like a lot! However, with Wasm, the code is downloaded and JIT-ted _simultaneously_ through the magic of streaming compilation. By the time the 50kb of Rust is finished downloading, it is already ready to go. Again, Dioxus beats out many benchmarks with time-to-interactivity.
|
||||
|
||||
For reference, Dioxus `hello-world` clocks in at around 70kb gzip or 60kb brotli, and Dioxus supports SSR.
|
||||
|
||||
### Why hooks? Why not MVC, classes, traits, messages, etc?
|
||||
There are plenty Rust Elm-like frameworks in the world - we were not interested in making another! Instead, we borrowed hooks from React. JS and Rust share many structural similarities, so if you're comfortable with React, then you'll be plenty comfortable with Dioxus.
|
||||
|
||||
### Why a custom DSL? Why not just pure function calls?
|
||||
The `RSX` DSL is _barely_ a DSL. Rustaceans will find the DSL very similar to simply assembling nested structs, but without the syntactical overhead of "Default" everywhere or having to jump through hoops with the builder pattern. Between RSX, HTML, the Raw Factory API, and the NodeBuilder syntax, there's plenty of options to choose from.
|
||||
|
||||
### What are the build times like? Why on earth would I choose Rust instead of JS/TS/Elm?
|
||||
Dioxus builds as roughly as fast as a complex WebPack-TypeScript site. Compile times will be slower than an equivalent TypeScript site, but not unbearably slow. The Wasm compiler backend for Rust is very fast. Iterating on small components is basically instant and larger apps takes a few seconds. In practice, the compiler guarantees of Rust balance out the rebuild times.
|
||||
|
||||
### What about Yew/Seed/Sycamore/Dominator/Dodrio/Percy?
|
||||
- Yew and Seed use an Elm-like pattern and don't support SSR or any alternate rendering platforms
|
||||
- Sycamore and Dominator are more like SolidJS/Svelte, requiring no VDOM but has less naturally-Rusty state management
|
||||
- Percy isn't quite mature yet
|
||||
- Dodrio is the spiritual predecessor of Dioxus, but is currently an archived research project without the batteries of Dioxus
|
||||
|
||||
### How do the mobile and desktop renderers work? Is it Electron?
|
||||
Currently, Dioxus uses your device's native WebView library to draw the page. None of your app code is actually running in the WebView thread, so you can access system resources instead of having to go through something like NodeJS. This means your app will use Safari on macOS/iOS, Edge (Chromium) on Windows, and whatever is the default Web Browser for Linux and Android. Because your code is compiled and running natively, performance is not a problem. You will have to use the various "Escape Hatches" to use browser-native APIs (like WebGL) and work around visual differences in how Safari and Chrome render the page.
|
||||
|
||||
In the future, we are interested in using Webrenderer to provide a fully native renderer without having to go through the system WebView library. In practice, Dioxus mobile and desktop are great for CRUD-style apps, but the ergonomic cross-platform APIs (GPS, Camera, etc) are not there yet.
|
||||
+1
-1
@@ -12,7 +12,7 @@ https://github.com/rustwasm/gloo
|
||||
For example, resize observer would function like this:
|
||||
|
||||
```rust
|
||||
pub static Example: FC<()> = |(cx, props)|{
|
||||
pub static Example: FC<()> = |cx, props|{
|
||||
let observer = use_resize_observer();
|
||||
|
||||
cx.render(rsx!(
|
||||
|
||||
@@ -153,13 +153,13 @@ Notice that LiveComponent receivers (the client-side interpretation of a LiveCom
|
||||
The `VNodeTree` type is a very special type that allows VNodes to be created using a pluggable allocator. The html! macro creates something that looks like:
|
||||
|
||||
```rust
|
||||
pub static Example: FC<()> = |(cx, props)|{
|
||||
pub static Example: FC<()> = |cx, props|{
|
||||
html! { <div> "blah" </div> }
|
||||
};
|
||||
|
||||
// expands to...
|
||||
|
||||
pub static Example: FC<()> = |(cx, props)|{
|
||||
pub static Example: FC<()> = |cx, props|{
|
||||
// This function converts a Fn(allocator) -> DomTree closure to a VNode struct that will later be evaluated.
|
||||
html_macro_to_vnodetree(move |allocator| {
|
||||
let mut node0 = allocator.alloc(VElement::div);
|
||||
@@ -313,7 +313,7 @@ Here's how react does it:
|
||||
Any "dirty" node causes an entire subtree render. Calling "setState" at the very top will cascade all the way down. This is particularly bad for this component design:
|
||||
|
||||
```rust
|
||||
static APP: FC<()> = |(cx, props)|{
|
||||
static APP: FC<()> = |cx, props|{
|
||||
let title = use_context(Title);
|
||||
cx.render(html!{
|
||||
<div>
|
||||
@@ -334,7 +334,7 @@ static APP: FC<()> = |(cx, props)|{
|
||||
</div>
|
||||
})
|
||||
};
|
||||
static HEAVY_LIST: FC<()> = |(cx, props)|{
|
||||
static HEAVY_LIST: FC<()> = |cx, props|{
|
||||
cx.render({
|
||||
{0.100.map(i => <BigElement >)}
|
||||
})
|
||||
@@ -378,7 +378,7 @@ struct Props {
|
||||
|
||||
}
|
||||
|
||||
static Component: FC<Props> = |(cx, props)|{
|
||||
static Component: FC<Props> = |cx, props|{
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
@@ -30,7 +30,7 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token
|
||||
///
|
||||
/// ## Complete Reference Guide:
|
||||
/// ```
|
||||
/// const Example: FC<()> = |(cx, props)|{
|
||||
/// const Example: FC<()> = |cx, props|{
|
||||
/// let formatting = "formatting!";
|
||||
/// let formatting_tuple = ("a", "b");
|
||||
/// let lazy_fmt = format_args!("lazily formatted text");
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::parse::{Parse, ParseStream};
|
||||
@@ -109,140 +111,104 @@ fn parse_variants_attributes(
|
||||
}
|
||||
|
||||
impl Routable {
|
||||
fn build_from_path(&self) -> TokenStream {
|
||||
let from_path_matches = self.variants.iter().enumerate().map(|(i, variant)| {
|
||||
let ident = &variant.ident;
|
||||
let right = match &variant.fields {
|
||||
Fields::Unit => quote! { Self::#ident },
|
||||
Fields::Named(field) => {
|
||||
let fields = field.named.iter().map(|it| {
|
||||
//named fields have idents
|
||||
it.ident.as_ref().unwrap()
|
||||
});
|
||||
quote! { Self::#ident { #(#fields: params.get(stringify!(#fields))?.parse().ok()?,)* } }
|
||||
}
|
||||
Fields::Unnamed(_) => unreachable!(), // already checked
|
||||
};
|
||||
// fn build_from_path(&self) -> TokenStream {
|
||||
// let from_path_matches = self.variants.iter().enumerate().map(|(i, variant)| {
|
||||
// let ident = &variant.ident;
|
||||
// let right = match &variant.fields {
|
||||
// Fields::Unit => quote! { Self::#ident },
|
||||
// Fields::Named(field) => {
|
||||
// let fields = field.named.iter().map(|it| {
|
||||
// //named fields have idents
|
||||
// it.ident.as_ref().unwrap()
|
||||
// });
|
||||
// quote! { Self::#ident { #(#fields: params.get(stringify!(#fields))?.parse().ok()?,)* } }
|
||||
// }
|
||||
// Fields::Unnamed(_) => unreachable!(), // already checked
|
||||
// };
|
||||
|
||||
let left = self.ats.get(i).unwrap();
|
||||
quote! {
|
||||
#left => ::std::option::Option::Some(#right)
|
||||
}
|
||||
});
|
||||
// let left = self.ats.get(i).unwrap();
|
||||
// quote! {
|
||||
// #left => ::std::option::Option::Some(#right)
|
||||
// }
|
||||
// });
|
||||
|
||||
quote! {
|
||||
fn from_path(path: &str, params: &::std::collections::HashMap<&str, &str>) -> ::std::option::Option<Self> {
|
||||
match path {
|
||||
#(#from_path_matches),*,
|
||||
_ => ::std::option::Option::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// quote! {
|
||||
// fn from_path(path: &str, params: &::std::collections::HashMap<&str, &str>) -> ::std::option::Option<Self> {
|
||||
// match path {
|
||||
// #(#from_path_matches),*,
|
||||
// _ => ::std::option::Option::None,
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
fn build_to_path(&self) -> TokenStream {
|
||||
let to_path_matches = self.variants.iter().enumerate().map(|(i, variant)| {
|
||||
let ident = &variant.ident;
|
||||
let mut right = self.ats.get(i).unwrap().value();
|
||||
// fn build_to_path(&self) -> TokenStream {
|
||||
// let to_path_matches = self.variants.iter().enumerate().map(|(i, variant)| {
|
||||
// let ident = &variant.ident;
|
||||
// let mut right = self.ats.get(i).unwrap().value();
|
||||
|
||||
match &variant.fields {
|
||||
Fields::Unit => quote! { Self::#ident => ::std::string::ToString::to_string(#right) },
|
||||
Fields::Named(field) => {
|
||||
let fields = field
|
||||
.named
|
||||
.iter()
|
||||
.map(|it| it.ident.as_ref().unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
// match &variant.fields {
|
||||
// Fields::Unit => quote! { Self::#ident => ::std::string::ToString::to_string(#right) },
|
||||
// Fields::Named(field) => {
|
||||
// let fields = field
|
||||
// .named
|
||||
// .iter()
|
||||
// .map(|it| it.ident.as_ref().unwrap())
|
||||
// .collect::<Vec<_>>();
|
||||
|
||||
for field in fields.iter() {
|
||||
// :param -> {param}
|
||||
// so we can pass it to `format!("...", param)`
|
||||
right = right.replace(&format!(":{}", field), &format!("{{{}}}", field))
|
||||
}
|
||||
// for field in fields.iter() {
|
||||
// // :param -> {param}
|
||||
// // so we can pass it to `format!("...", param)`
|
||||
// right = right.replace(&format!(":{}", field), &format!("{{{}}}", field))
|
||||
// }
|
||||
|
||||
quote! {
|
||||
Self::#ident { #(#fields),* } => ::std::format!(#right, #(#fields = #fields),*)
|
||||
}
|
||||
}
|
||||
Fields::Unnamed(_) => unreachable!(), // already checked
|
||||
}
|
||||
});
|
||||
// quote! {
|
||||
// Self::#ident { #(#fields),* } => ::std::format!(#right, #(#fields = #fields),*)
|
||||
// }
|
||||
// }
|
||||
// Fields::Unnamed(_) => unreachable!(), // already checked
|
||||
// }
|
||||
// });
|
||||
|
||||
quote! {
|
||||
fn to_path(&self) -> ::std::string::String {
|
||||
match self {
|
||||
#(#to_path_matches),*,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// quote! {
|
||||
// fn to_path(&self) -> ::std::string::String {
|
||||
// match self {
|
||||
// #(#to_path_matches),*,
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
pub fn routable_derive_impl(input: Routable) -> TokenStream {
|
||||
let Routable {
|
||||
ats,
|
||||
not_found_route,
|
||||
ident,
|
||||
// ats,
|
||||
// not_found_route,
|
||||
// ident,
|
||||
..
|
||||
} = &input;
|
||||
|
||||
let from_path = input.build_from_path();
|
||||
let to_path = input.build_to_path();
|
||||
|
||||
let not_found_route = match not_found_route {
|
||||
Some(route) => quote! { ::std::option::Option::Some(Self::#route) },
|
||||
None => quote! { ::std::option::Option::None },
|
||||
};
|
||||
|
||||
let cache_thread_local_ident = Ident::new(
|
||||
&format!("__{}_ROUTER_CURRENT_ROUTE_CACHE", ident),
|
||||
ident.span(),
|
||||
);
|
||||
// let from_path = input.build_from_path();
|
||||
// let to_path = input.build_to_path();
|
||||
|
||||
quote! {
|
||||
// ::std::thread_local! {
|
||||
// #[doc(hidden)]
|
||||
// #[allow(non_upper_case_globals)]
|
||||
// static #cache_thread_local_ident: ::std::cell::RefCell<::std::option::Option<#ident>> = ::std::cell::RefCell::new(::std::option::Option::None);
|
||||
// #[automatically_derived]
|
||||
// impl ::dioxus::router::Routable for #ident {
|
||||
|
||||
// fn recognize(pathname: &str) -> ::std::option::Option<Self> {
|
||||
// todo!()
|
||||
// // ::std::thread_local! {
|
||||
// // static ROUTER: ::dioxus::router::__macro::Router = ::dioxus::router::__macro::build_router::<#ident>();
|
||||
// // }
|
||||
// // let route = ROUTER.with(|router| ::dioxus::router::__macro::recognize_with_router(router, pathname));
|
||||
// // {
|
||||
// // let route = ::std::clone::Clone::clone(&route);
|
||||
// // #cache_thread_local_ident.with(move |val| {
|
||||
// // *val.borrow_mut() = route;
|
||||
// // });
|
||||
// // }
|
||||
// // route
|
||||
// }
|
||||
// }
|
||||
|
||||
#[automatically_derived]
|
||||
impl ::dioxus::router::Routable for #ident {
|
||||
#from_path
|
||||
#to_path
|
||||
|
||||
fn routes() -> ::std::vec::Vec<&'static str> {
|
||||
::std::vec![#(#ats),*]
|
||||
}
|
||||
|
||||
fn not_found_route() -> ::std::option::Option<Self> {
|
||||
#not_found_route
|
||||
}
|
||||
|
||||
// fn current_route() -> ::std::option::Option<Self> {
|
||||
// #cache_thread_local_ident.with(|val| ::std::clone::Clone::clone(&*val.borrow()))
|
||||
// }
|
||||
|
||||
fn recognize(pathname: &str) -> ::std::option::Option<Self> {
|
||||
todo!()
|
||||
// ::std::thread_local! {
|
||||
// static ROUTER: ::dioxus::router::__macro::Router = ::dioxus::router::__macro::build_router::<#ident>();
|
||||
// }
|
||||
// let route = ROUTER.with(|router| ::dioxus::router::__macro::recognize_with_router(router, pathname));
|
||||
// {
|
||||
// let route = ::std::clone::Clone::clone(&route);
|
||||
// #cache_thread_local_ident.with(move |val| {
|
||||
// *val.borrow_mut() = route;
|
||||
// });
|
||||
// }
|
||||
// route
|
||||
}
|
||||
|
||||
// fn cleanup() {
|
||||
// #cache_thread_local_ident.with(move |val| {
|
||||
// *val.borrow_mut() = ::std::option::Option::None;
|
||||
// });
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,19 +49,17 @@ impl ToTokens for CallBody {
|
||||
match &self.custom_context {
|
||||
// The `in cx` pattern allows directly rendering
|
||||
Some(ident) => out_tokens.append_all(quote! {
|
||||
#ident.render(NodeFactory::annotate_lazy(move |__cx: NodeFactory| -> VNode {
|
||||
#ident.render(LazyNodes::new_some(move |__cx: NodeFactory| -> VNode {
|
||||
use dioxus_elements::{GlobalAttributes, SvgAttributes};
|
||||
#inner
|
||||
}))
|
||||
}),
|
||||
// Otherwise we just build the LazyNode wrapper
|
||||
None => out_tokens.append_all(quote! {
|
||||
{
|
||||
NodeFactory::annotate_lazy(move |__cx: NodeFactory| -> VNode {
|
||||
use dioxus_elements::{GlobalAttributes, SvgAttributes};
|
||||
#inner
|
||||
})
|
||||
}
|
||||
LazyNodes::new_some(move |__cx: NodeFactory| -> VNode {
|
||||
use dioxus_elements::{GlobalAttributes, SvgAttributes};
|
||||
#inner
|
||||
})
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ impl ToTokens for RsxTemplate {
|
||||
|
||||
// // create a lazy tree that accepts a bump allocator
|
||||
// let final_tokens = quote! {
|
||||
// dioxus::prelude::LazyNodes::new(move |(cx, props)|{
|
||||
// dioxus::prelude::LazyNodes::new(move |cx, props|{
|
||||
// let bump = &cx.bump();
|
||||
|
||||
// #new_toks
|
||||
|
||||
@@ -1,264 +0,0 @@
|
||||
//! The `calc` functionality.
|
||||
use crate::LengthPercentage;
|
||||
use ::{
|
||||
proc_macro2::TokenStream,
|
||||
quote::{quote, ToTokens},
|
||||
std::fmt,
|
||||
syn::{
|
||||
custom_keyword, parenthesized,
|
||||
parse::{Parse, ParseStream},
|
||||
Token,
|
||||
},
|
||||
};
|
||||
|
||||
/// Values that can be a calculaion (currently restricted to length & percentages)
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Calc {
|
||||
Calculated(CalcSum),
|
||||
Normal(LengthPercentage),
|
||||
}
|
||||
|
||||
impl fmt::Display for Calc {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Calc::Calculated(inner) => write!(f, "calc({})", inner),
|
||||
Calc::Normal(inner) => write!(f, "{}", inner),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for Calc {
|
||||
fn parse(s: ParseStream) -> syn::Result<Self> {
|
||||
custom_keyword!(calc);
|
||||
if s.peek(calc) {
|
||||
s.parse::<calc>()?;
|
||||
let content;
|
||||
parenthesized!(content in s);
|
||||
Ok(Calc::Calculated(content.parse()?))
|
||||
} else {
|
||||
Ok(Calc::Normal(s.parse()?))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for Calc {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
tokens.extend(match self {
|
||||
Calc::Calculated(inner) => quote!(style::Calc::Calculated(#inner)),
|
||||
Calc::Normal(inner) => quote!(style::Calc::Normal(#inner)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_calc() {
|
||||
for (input, output) in vec![
|
||||
("calc(10% - 20\"em\")", "calc(10% - 20em)"),
|
||||
("calc(100% + 5px)", "calc(100% + 5px)"),
|
||||
("calc(100% - 60px)", "calc(100% - 60px)"),
|
||||
] {
|
||||
assert_eq!(&syn::parse_str::<Calc>(input).unwrap().to_string(), output);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct CalcSum {
|
||||
pub first: CalcProduct,
|
||||
pub rest: Vec<SumOp>,
|
||||
}
|
||||
|
||||
impl fmt::Display for CalcSum {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.first)?;
|
||||
for op in self.rest.iter() {
|
||||
write!(f, "{}", op)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for CalcSum {
|
||||
fn parse(s: ParseStream) -> syn::Result<Self> {
|
||||
let first: CalcProduct = s.parse()?;
|
||||
let mut rest: Vec<SumOp> = vec![];
|
||||
while SumOp::peek(s) {
|
||||
rest.push(s.parse()?);
|
||||
}
|
||||
Ok(CalcSum { first, rest })
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for CalcSum {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let first = &self.first;
|
||||
let rest = self.rest.iter();
|
||||
tokens.extend(quote! {
|
||||
style::calc::CalcSum {
|
||||
first: #first,
|
||||
rest: vec![#(#rest,)*]
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum SumOp {
|
||||
Add(CalcProduct),
|
||||
Sub(CalcProduct),
|
||||
}
|
||||
|
||||
impl SumOp {
|
||||
fn peek(s: ParseStream) -> bool {
|
||||
s.peek(Token![+]) || s.peek(Token![-])
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for SumOp {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
SumOp::Add(inner) => write!(f, " + {}", inner),
|
||||
SumOp::Sub(inner) => write!(f, " - {}", inner),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for SumOp {
|
||||
fn parse(s: ParseStream) -> syn::Result<Self> {
|
||||
let lookahead = s.lookahead1();
|
||||
if lookahead.peek(Token![+]) {
|
||||
s.parse::<Token![+]>()?;
|
||||
Ok(SumOp::Add(s.parse()?))
|
||||
} else if lookahead.peek(Token![-]) {
|
||||
s.parse::<Token![-]>()?;
|
||||
Ok(SumOp::Sub(s.parse()?))
|
||||
} else {
|
||||
Err(lookahead.error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for SumOp {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
tokens.extend(match self {
|
||||
SumOp::Add(inner) => quote!(style::SumOp::Add(#inner)),
|
||||
SumOp::Sub(inner) => quote!(style::SumOp::Sub(#inner)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct CalcProduct {
|
||||
pub first: CalcValue,
|
||||
pub rest: Vec<ProductOp>,
|
||||
}
|
||||
|
||||
impl fmt::Display for CalcProduct {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.first)?;
|
||||
for op in self.rest.iter() {
|
||||
write!(f, "{}", op)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for CalcProduct {
|
||||
fn parse(s: ParseStream) -> syn::Result<Self> {
|
||||
let first: CalcValue = s.parse()?;
|
||||
let mut rest: Vec<ProductOp> = vec![];
|
||||
while ProductOp::peek(s) {
|
||||
rest.push(s.parse()?);
|
||||
}
|
||||
Ok(CalcProduct { first, rest })
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for CalcProduct {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let first = &self.first;
|
||||
let rest = self.rest.iter();
|
||||
tokens.extend(quote! {
|
||||
style::calc::CalcProduct {
|
||||
first: #first,
|
||||
rest: vec![#(#rest,)*]
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum ProductOp {
|
||||
Mul(CalcValue),
|
||||
// todo Div(Number),
|
||||
}
|
||||
|
||||
impl ProductOp {
|
||||
pub fn peek(s: ParseStream) -> bool {
|
||||
s.peek(Token![*]) // || s.peek(Token[/])
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ProductOp {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
ProductOp::Mul(inner) => write!(f, "*{}", inner),
|
||||
//ProductOp::Div(inner) => write!(f, "/{}", inner),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for ProductOp {
|
||||
fn parse(s: ParseStream) -> syn::Result<Self> {
|
||||
let lookahead = s.lookahead1();
|
||||
if lookahead.peek(Token![*]) {
|
||||
s.parse::<Token![*]>()?;
|
||||
Ok(ProductOp::Mul(s.parse()?))
|
||||
/*
|
||||
} else if lookahead.peek(Token![/]) {
|
||||
s.parse::<Token![/]>()?;
|
||||
Ok(ProductOp::Div(s.parse()?))
|
||||
*/
|
||||
} else {
|
||||
Err(lookahead.error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for ProductOp {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
tokens.extend(match self {
|
||||
ProductOp::Mul(inner) => quote!(style::ProductOp::Mul(#inner)),
|
||||
//ProductOp::Div(inner) => quote!(style::ProductOp::Div(#inner)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum CalcValue {
|
||||
LengthPercentage(LengthPercentage),
|
||||
// todo more variants
|
||||
}
|
||||
|
||||
impl Parse for CalcValue {
|
||||
fn parse(s: ParseStream) -> syn::Result<Self> {
|
||||
Ok(CalcValue::LengthPercentage(s.parse()?))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for CalcValue {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
CalcValue::LengthPercentage(inner) => write!(f, "{}", inner),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for CalcValue {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
tokens.extend(match self {
|
||||
CalcValue::LengthPercentage(inner) => {
|
||||
quote!(style::CalcValue::LengthPercentage(#inner))
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,854 +0,0 @@
|
||||
use std::fmt;
|
||||
|
||||
/// A color that possibly is possibly code, rather than a literal
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum DynamicColor {
|
||||
Literal(Color),
|
||||
/// The type of the block is not checked here (it is checked by typeck).
|
||||
Dynamic(syn::Block),
|
||||
}
|
||||
|
||||
impl DynamicColor {
|
||||
pub fn is_dynamic(&self) -> bool {
|
||||
match self {
|
||||
DynamicColor::Dynamic(_) => true,
|
||||
DynamicColor::Literal(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for DynamicColor {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
DynamicColor::Dynamic(_) => Ok(()),
|
||||
DynamicColor::Literal(color) => color.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO other color variants.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub enum Color {
|
||||
HexRGB(u8, u8, u8),
|
||||
HexRGBA(u8, u8, u8, u8),
|
||||
// Invariants: `0 <= .0 < 360`, `0 <= .1 < 100`, `0 <= .2 < 100`.
|
||||
HSL(f64, f64, f64),
|
||||
// Invariants: `0 <= .0 < 360`, `0 <= .1 < 100`, `0 <= .2 < 100`, `0 <= .3 < 1`.
|
||||
HSLA(f64, f64, f64, f64),
|
||||
|
||||
// Red HTML Color Names
|
||||
/// rgb(205, 92, 92)
|
||||
IndianRed,
|
||||
/// rgb(240, 128, 128)
|
||||
LightCoral,
|
||||
/// rgb(250, 128, 114)
|
||||
Salmon,
|
||||
/// rgb(233, 150, 122)
|
||||
DarkSalmon,
|
||||
/// rgb(255, 160, 122)
|
||||
LightSalmon,
|
||||
/// rgb(220, 20, 60)
|
||||
Crimson,
|
||||
/// rgb(255, 0, 0)
|
||||
Red,
|
||||
/// rgb(178, 34, 34)
|
||||
FireBrick,
|
||||
/// rgb(139, 0, 0)
|
||||
DarkRed,
|
||||
// Pink HTML Color Names
|
||||
/// rgb(255, 192, 203)
|
||||
Pink,
|
||||
/// rgb(255, 182, 193)
|
||||
LightPink,
|
||||
/// rgb(255, 105, 180)
|
||||
HotPink,
|
||||
/// rgb(255, 20, 147)
|
||||
DeepPink,
|
||||
/// rgb(199, 21, 133)
|
||||
MediumVioletRed,
|
||||
/// rgb(219, 112, 147)
|
||||
PaleVioletRed,
|
||||
//Orange HTML Color Names
|
||||
// /// rgb(255, 160, 122) redefined
|
||||
// LightSalmon,
|
||||
/// rgb(255, 127, 80)
|
||||
Coral,
|
||||
/// rgb(255, 99, 71)
|
||||
Tomato,
|
||||
/// rgb(255, 69, 0)
|
||||
OrangeRed,
|
||||
/// rgb(255, 140, 0)
|
||||
DarkOrange,
|
||||
/// rgb(255, 165, 0)
|
||||
Orange,
|
||||
// Yellow HTML Color Names
|
||||
/// rgb(255, 215, 0)
|
||||
Gold,
|
||||
/// rgb(255, 255, 0)
|
||||
Yellow,
|
||||
/// rgb(255, 255, 224)
|
||||
LightYellow,
|
||||
/// rgb(255, 250, 205)
|
||||
LemonChiffon,
|
||||
/// rgb(250, 250, 210)
|
||||
LightGoldenrodYellow,
|
||||
/// rgb(255, 239, 213)
|
||||
PapayaWhip,
|
||||
/// rgb(255, 228, 181)
|
||||
Moccasin,
|
||||
/// rgb(255, 218, 185)
|
||||
PeachPuff,
|
||||
/// rgb(238, 232, 170)
|
||||
PaleGoldenrod,
|
||||
/// rgb(240, 230, 140)
|
||||
Khaki,
|
||||
/// rgb(189, 183, 107)
|
||||
DarkKhaki,
|
||||
// Purple HTML Color Names
|
||||
/// rgb(230, 230, 250)
|
||||
Lavender,
|
||||
/// rgb(216, 191, 216)
|
||||
Thistle,
|
||||
/// rgb(221, 160, 221)
|
||||
Plum,
|
||||
/// rgb(238, 130, 238)
|
||||
Violet,
|
||||
/// rgb(218, 112, 214)
|
||||
Orchid,
|
||||
/// rgb(255, 0, 255)
|
||||
Fuchsia,
|
||||
/// rgb(255, 0, 255)
|
||||
Magenta,
|
||||
/// rgb(186, 85, 211)
|
||||
MediumOrchid,
|
||||
/// rgb(147, 112, 219)
|
||||
MediumPurple,
|
||||
/// rgb(102, 51, 153)
|
||||
RebeccaPurple,
|
||||
/// rgb(138, 43, 226)
|
||||
BlueViolet,
|
||||
/// rgb(148, 0, 211)
|
||||
DarkViolet,
|
||||
/// rgb(153, 50, 204)
|
||||
DarkOrchid,
|
||||
/// rgb(139, 0, 139)
|
||||
DarkMagenta,
|
||||
/// rgb(128, 0, 128)
|
||||
Purple,
|
||||
/// rgb(75, 0, 130)
|
||||
Indigo,
|
||||
/// rgb(106, 90, 205)
|
||||
SlateBlue,
|
||||
/// rgb(72, 61, 139)
|
||||
DarkSlateBlue,
|
||||
/// rgb(123, 104, 238)
|
||||
MediumSlateBlue,
|
||||
// Green HTML Color Names
|
||||
/// rgb(173, 255, 47)
|
||||
GreenYellow,
|
||||
/// rgb(127, 255, 0)
|
||||
Chartreuse,
|
||||
/// rgb(124, 252, 0)
|
||||
LawnGreen,
|
||||
/// rgb(0, 255, 0)
|
||||
Lime,
|
||||
/// rgb(50, 205, 50)
|
||||
LimeGreen,
|
||||
/// rgb(152, 251, 152)
|
||||
PaleGreen,
|
||||
/// rgb(144, 238, 144)
|
||||
LightGreen,
|
||||
/// rgb(0, 250, 154)
|
||||
MediumSpringGreen,
|
||||
/// rgb(0, 255, 127)
|
||||
SpringGreen,
|
||||
/// rgb(60, 179, 113)
|
||||
MediumSeaGreen,
|
||||
/// rgb(46, 139, 87)
|
||||
SeaGreen,
|
||||
/// rgb(34, 139, 34)
|
||||
ForestGreen,
|
||||
/// rgb(0, 128, 0)
|
||||
Green,
|
||||
/// rgb(0, 100, 0)
|
||||
DarkGreen,
|
||||
/// rgb(154, 205, 50)
|
||||
YellowGreen,
|
||||
/// rgb(107, 142, 35)
|
||||
OliveDrab,
|
||||
/// rgb(128, 128, 0)
|
||||
Olive,
|
||||
/// rgb(85, 107, 47)
|
||||
DarkOliveGreen,
|
||||
/// rgb(102, 205, 170)
|
||||
MediumAquamarine,
|
||||
/// rgb(143, 188, 139)
|
||||
DarkSeaGreen,
|
||||
/// rgb(32, 178, 170)
|
||||
LightSeaGreen,
|
||||
/// rgb(0, 139, 139)
|
||||
DarkCyan,
|
||||
/// rgb(0, 128, 128)
|
||||
Teal,
|
||||
// Blue HTML Color Names
|
||||
/// rgb(0, 255, 255)
|
||||
Aqua,
|
||||
/// rgb(0, 255, 255)
|
||||
Cyan,
|
||||
/// rgb(224, 255, 255)
|
||||
LightCyan,
|
||||
/// rgb(175, 238, 238)
|
||||
PaleTurquoise,
|
||||
/// rgb(127, 255, 212)
|
||||
Aquamarine,
|
||||
/// rgb(64, 224, 208)
|
||||
Turquoise,
|
||||
/// rgb(72, 209, 204)
|
||||
MediumTurquoise,
|
||||
/// rgb(0, 206, 209)
|
||||
DarkTurquoise,
|
||||
/// rgb(95, 158, 160)
|
||||
CadetBlue,
|
||||
/// rgb(70, 130, 180)
|
||||
SteelBlue,
|
||||
/// rgb(176, 196, 222)
|
||||
LightSteelBlue,
|
||||
/// rgb(176, 224, 230)
|
||||
PowderBlue,
|
||||
/// rgb(173, 216, 230)
|
||||
LightBlue,
|
||||
/// rgb(135, 206, 235)
|
||||
SkyBlue,
|
||||
/// rgb(135, 206, 250)
|
||||
LightSkyBlue,
|
||||
/// rgb(0, 191, 255)
|
||||
DeepSkyBlue,
|
||||
/// rgb(30, 144, 255)
|
||||
DodgerBlue,
|
||||
/// rgb(100, 149, 237)
|
||||
CornflowerBlue,
|
||||
// /// rgb(123, 104, 238) duplicate
|
||||
//MediumSlateBlue,
|
||||
/// rgb(65, 105, 225)
|
||||
RoyalBlue,
|
||||
/// rgb(0, 0, 255)
|
||||
Blue,
|
||||
/// rgb(0, 0, 205)
|
||||
MediumBlue,
|
||||
/// rgb(0, 0, 139)
|
||||
DarkBlue,
|
||||
/// rgb(0, 0, 128)
|
||||
Navy,
|
||||
/// rgb(25, 25, 112)
|
||||
MidnightBlue,
|
||||
// Brown HTML Color Names
|
||||
/// rgb(255, 248, 220)
|
||||
Cornsilk,
|
||||
/// rgb(255, 235, 205)
|
||||
BlanchedAlmond,
|
||||
/// rgb(255, 228, 196)
|
||||
Bisque,
|
||||
/// rgb(255, 222, 173)
|
||||
NavajoWhite,
|
||||
/// rgb(245, 222, 179)
|
||||
Wheat,
|
||||
/// rgb(222, 184, 135)
|
||||
BurlyWood,
|
||||
/// rgb(210, 180, 140)
|
||||
Tan,
|
||||
/// rgb(188, 143, 143)
|
||||
RosyBrown,
|
||||
/// rgb(244, 164, 96)
|
||||
SandyBrown,
|
||||
/// rgb(218, 165, 32)
|
||||
Goldenrod,
|
||||
/// rgb(184, 134, 11)
|
||||
DarkGoldenrod,
|
||||
/// rgb(205, 133, 63)
|
||||
Peru,
|
||||
/// rgb(210, 105, 30)
|
||||
Chocolate,
|
||||
/// rgb(139, 69, 19)
|
||||
SaddleBrown,
|
||||
/// rgb(160, 82, 45)
|
||||
Sienna,
|
||||
/// rgb(165, 42, 42)
|
||||
Brown,
|
||||
/// rgb(128, 0, 0)
|
||||
Maroon,
|
||||
// White HTML Color Names
|
||||
/// rgb(255, 255, 255)
|
||||
White,
|
||||
/// rgb(255, 250, 250)
|
||||
Snow,
|
||||
/// rgb(240, 255, 240)
|
||||
HoneyDew,
|
||||
/// rgb(245, 255, 250)
|
||||
MintCream,
|
||||
/// rgb(240, 255, 255)
|
||||
Azure,
|
||||
/// rgb(240, 248, 255)
|
||||
AliceBlue,
|
||||
/// rgb(248, 248, 255)
|
||||
GhostWhite,
|
||||
/// rgb(245, 245, 245)
|
||||
WhiteSmoke,
|
||||
/// rgb(255, 245, 238)
|
||||
SeaShell,
|
||||
/// rgb(245, 245, 220)
|
||||
Beige,
|
||||
/// rgb(253, 245, 230)
|
||||
OldLace,
|
||||
/// rgb(255, 250, 240)
|
||||
FloralWhite,
|
||||
/// rgb(255, 255, 240)
|
||||
Ivory,
|
||||
/// rgb(250, 235, 215)
|
||||
AntiqueWhite,
|
||||
/// rgb(250, 240, 230)
|
||||
Linen,
|
||||
/// rgb(255, 240, 245)
|
||||
LavenderBlush,
|
||||
/// rgb(255, 228, 225)
|
||||
MistyRose,
|
||||
// Gray HTML Color Names
|
||||
/// rgb(220, 220, 220)
|
||||
Gainsboro,
|
||||
/// rgb(211, 211, 211)
|
||||
LightGray,
|
||||
/// rgb(192, 192, 192)
|
||||
Silver,
|
||||
/// rgb(169, 169, 169)
|
||||
DarkGray,
|
||||
/// rgb(128, 128, 128)
|
||||
Gray,
|
||||
/// rgb(105, 105, 105)
|
||||
DimGray,
|
||||
/// rgb(119, 136, 153)
|
||||
LightSlateGray,
|
||||
/// rgb(112, 128, 144)
|
||||
SlateGray,
|
||||
/// rgb(47, 79, 79)
|
||||
DarkSlateGray,
|
||||
/// rgb(0, 0, 0)
|
||||
Black,
|
||||
}
|
||||
|
||||
impl Color {
|
||||
// todo similar for others
|
||||
pub fn to_rgb(self) -> Color {
|
||||
use Color::*;
|
||||
match self {
|
||||
HexRGB(r, g, b) => HexRGB(r, g, b),
|
||||
HexRGBA(r, g, b, _) => HexRGB(r, g, b),
|
||||
HSL(h, s, l) => {
|
||||
let s = s * 0.01; // percent conversion
|
||||
let l = l * 0.01; // percent conversion
|
||||
let (r, g, b) = hsl_to_rgb(h, s, l);
|
||||
HexRGB((r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8)
|
||||
}
|
||||
HSLA(h, s, l, _) => Color::to_rgb(HSL(h, s, l)),
|
||||
IndianRed => HexRGB(205, 92, 92),
|
||||
LightCoral => HexRGB(240, 128, 128),
|
||||
Salmon => HexRGB(250, 128, 114),
|
||||
DarkSalmon => HexRGB(233, 150, 122),
|
||||
LightSalmon => HexRGB(255, 160, 122),
|
||||
Crimson => HexRGB(220, 20, 60),
|
||||
Red => HexRGB(255, 0, 0),
|
||||
FireBrick => HexRGB(178, 34, 34),
|
||||
DarkRed => HexRGB(139, 0, 0),
|
||||
Pink => HexRGB(255, 192, 203),
|
||||
LightPink => HexRGB(255, 182, 193),
|
||||
HotPink => HexRGB(255, 105, 180),
|
||||
DeepPink => HexRGB(255, 20, 147),
|
||||
MediumVioletRed => HexRGB(199, 21, 133),
|
||||
PaleVioletRed => HexRGB(219, 112, 147),
|
||||
Coral => HexRGB(255, 127, 80),
|
||||
Tomato => HexRGB(255, 99, 71),
|
||||
OrangeRed => HexRGB(255, 69, 0),
|
||||
DarkOrange => HexRGB(255, 140, 0),
|
||||
Orange => HexRGB(255, 165, 0),
|
||||
Gold => HexRGB(255, 215, 0),
|
||||
Yellow => HexRGB(255, 255, 0),
|
||||
LightYellow => HexRGB(255, 255, 224),
|
||||
LemonChiffon => HexRGB(255, 250, 205),
|
||||
LightGoldenrodYellow => HexRGB(250, 250, 210),
|
||||
PapayaWhip => HexRGB(255, 239, 213),
|
||||
Moccasin => HexRGB(255, 228, 181),
|
||||
PeachPuff => HexRGB(255, 218, 185),
|
||||
PaleGoldenrod => HexRGB(238, 232, 170),
|
||||
Khaki => HexRGB(240, 230, 140),
|
||||
DarkKhaki => HexRGB(189, 183, 107),
|
||||
Lavender => HexRGB(230, 230, 250),
|
||||
Thistle => HexRGB(216, 191, 216),
|
||||
Plum => HexRGB(221, 160, 221),
|
||||
Violet => HexRGB(238, 130, 238),
|
||||
Orchid => HexRGB(218, 112, 214),
|
||||
Fuchsia => HexRGB(255, 0, 255),
|
||||
Magenta => HexRGB(255, 0, 255),
|
||||
MediumOrchid => HexRGB(186, 85, 211),
|
||||
MediumPurple => HexRGB(147, 112, 219),
|
||||
RebeccaPurple => HexRGB(102, 51, 153),
|
||||
BlueViolet => HexRGB(138, 43, 226),
|
||||
DarkViolet => HexRGB(148, 0, 211),
|
||||
DarkOrchid => HexRGB(153, 50, 204),
|
||||
DarkMagenta => HexRGB(139, 0, 139),
|
||||
Purple => HexRGB(128, 0, 128),
|
||||
Indigo => HexRGB(75, 0, 130),
|
||||
SlateBlue => HexRGB(106, 90, 205),
|
||||
DarkSlateBlue => HexRGB(72, 61, 139),
|
||||
MediumSlateBlue => HexRGB(123, 104, 238),
|
||||
GreenYellow => HexRGB(173, 255, 47),
|
||||
Chartreuse => HexRGB(127, 255, 0),
|
||||
LawnGreen => HexRGB(124, 252, 0),
|
||||
Lime => HexRGB(0, 255, 0),
|
||||
LimeGreen => HexRGB(50, 205, 50),
|
||||
PaleGreen => HexRGB(152, 251, 152),
|
||||
LightGreen => HexRGB(144, 238, 144),
|
||||
MediumSpringGreen => HexRGB(0, 250, 154),
|
||||
SpringGreen => HexRGB(0, 255, 127),
|
||||
MediumSeaGreen => HexRGB(60, 179, 113),
|
||||
SeaGreen => HexRGB(46, 139, 87),
|
||||
ForestGreen => HexRGB(34, 139, 34),
|
||||
Green => HexRGB(0, 128, 0),
|
||||
DarkGreen => HexRGB(0, 100, 0),
|
||||
YellowGreen => HexRGB(154, 205, 50),
|
||||
OliveDrab => HexRGB(107, 142, 35),
|
||||
Olive => HexRGB(128, 128, 0),
|
||||
DarkOliveGreen => HexRGB(85, 107, 47),
|
||||
MediumAquamarine => HexRGB(102, 205, 170),
|
||||
DarkSeaGreen => HexRGB(143, 188, 139),
|
||||
LightSeaGreen => HexRGB(32, 178, 170),
|
||||
DarkCyan => HexRGB(0, 139, 139),
|
||||
Teal => HexRGB(0, 128, 128),
|
||||
Aqua => HexRGB(0, 255, 255),
|
||||
Cyan => HexRGB(0, 255, 255),
|
||||
LightCyan => HexRGB(224, 255, 255),
|
||||
PaleTurquoise => HexRGB(175, 238, 238),
|
||||
Aquamarine => HexRGB(127, 255, 212),
|
||||
Turquoise => HexRGB(64, 224, 208),
|
||||
MediumTurquoise => HexRGB(72, 209, 204),
|
||||
DarkTurquoise => HexRGB(0, 206, 209),
|
||||
CadetBlue => HexRGB(95, 158, 160),
|
||||
SteelBlue => HexRGB(70, 130, 180),
|
||||
LightSteelBlue => HexRGB(176, 196, 222),
|
||||
PowderBlue => HexRGB(176, 224, 230),
|
||||
LightBlue => HexRGB(173, 216, 230),
|
||||
SkyBlue => HexRGB(135, 206, 235),
|
||||
LightSkyBlue => HexRGB(135, 206, 250),
|
||||
DeepSkyBlue => HexRGB(0, 191, 255),
|
||||
DodgerBlue => HexRGB(30, 144, 255),
|
||||
CornflowerBlue => HexRGB(100, 149, 237),
|
||||
RoyalBlue => HexRGB(65, 105, 225),
|
||||
Blue => HexRGB(0, 0, 255),
|
||||
MediumBlue => HexRGB(0, 0, 205),
|
||||
DarkBlue => HexRGB(0, 0, 139),
|
||||
Navy => HexRGB(0, 0, 128),
|
||||
MidnightBlue => HexRGB(25, 25, 112),
|
||||
Cornsilk => HexRGB(255, 248, 220),
|
||||
BlanchedAlmond => HexRGB(255, 235, 205),
|
||||
Bisque => HexRGB(255, 228, 196),
|
||||
NavajoWhite => HexRGB(255, 222, 173),
|
||||
Wheat => HexRGB(245, 222, 179),
|
||||
BurlyWood => HexRGB(222, 184, 135),
|
||||
Tan => HexRGB(210, 180, 140),
|
||||
RosyBrown => HexRGB(188, 143, 143),
|
||||
SandyBrown => HexRGB(244, 164, 96),
|
||||
Goldenrod => HexRGB(218, 165, 32),
|
||||
DarkGoldenrod => HexRGB(184, 134, 11),
|
||||
Peru => HexRGB(205, 133, 63),
|
||||
Chocolate => HexRGB(210, 105, 30),
|
||||
SaddleBrown => HexRGB(139, 69, 19),
|
||||
Sienna => HexRGB(160, 82, 45),
|
||||
Brown => HexRGB(165, 42, 42),
|
||||
Maroon => HexRGB(128, 0, 0),
|
||||
White => HexRGB(255, 255, 255),
|
||||
Snow => HexRGB(255, 250, 250),
|
||||
HoneyDew => HexRGB(240, 255, 240),
|
||||
MintCream => HexRGB(245, 255, 250),
|
||||
Azure => HexRGB(240, 255, 255),
|
||||
AliceBlue => HexRGB(240, 248, 255),
|
||||
GhostWhite => HexRGB(248, 248, 255),
|
||||
WhiteSmoke => HexRGB(245, 245, 245),
|
||||
SeaShell => HexRGB(255, 245, 238),
|
||||
Beige => HexRGB(245, 245, 220),
|
||||
OldLace => HexRGB(253, 245, 230),
|
||||
FloralWhite => HexRGB(255, 250, 240),
|
||||
Ivory => HexRGB(255, 255, 240),
|
||||
AntiqueWhite => HexRGB(250, 235, 215),
|
||||
Linen => HexRGB(250, 240, 230),
|
||||
LavenderBlush => HexRGB(255, 240, 245),
|
||||
MistyRose => HexRGB(255, 228, 225),
|
||||
Gainsboro => HexRGB(220, 220, 220),
|
||||
LightGray => HexRGB(211, 211, 211),
|
||||
Silver => HexRGB(192, 192, 192),
|
||||
DarkGray => HexRGB(169, 169, 169),
|
||||
Gray => HexRGB(128, 128, 128),
|
||||
DimGray => HexRGB(105, 105, 105),
|
||||
LightSlateGray => HexRGB(119, 136, 153),
|
||||
SlateGray => HexRGB(112, 128, 144),
|
||||
DarkSlateGray => HexRGB(47, 79, 79),
|
||||
Black => HexRGB(0, 0, 0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_named(name: &str) -> Option<Self> {
|
||||
// todo use a faster search (e.g. hashmap, aho-corasick)
|
||||
use Color::*;
|
||||
Some(match name {
|
||||
"indianred" => IndianRed,
|
||||
"lightcoral" => LightCoral,
|
||||
"salmon" => Salmon,
|
||||
"darksalmon" => DarkSalmon,
|
||||
"lightsalmon" => LightSalmon,
|
||||
"crimson" => Crimson,
|
||||
"red" => Red,
|
||||
"firebrick" => FireBrick,
|
||||
"darkred" => DarkRed,
|
||||
"pink" => Pink,
|
||||
"lightpink" => LightPink,
|
||||
"hotpink" => HotPink,
|
||||
"deeppink" => DeepPink,
|
||||
"mediumvioletred" => MediumVioletRed,
|
||||
"palevioletred" => PaleVioletRed,
|
||||
"coral" => Coral,
|
||||
"tomato" => Tomato,
|
||||
"orangered" => OrangeRed,
|
||||
"darkorange" => DarkOrange,
|
||||
"orange" => Orange,
|
||||
"gold" => Gold,
|
||||
"yellow" => Yellow,
|
||||
"lightyellow" => LightYellow,
|
||||
"lemonchiffon" => LemonChiffon,
|
||||
"lightgoldenrodyellow" => LightGoldenrodYellow,
|
||||
"papayawhip" => PapayaWhip,
|
||||
"Moccasin" => Moccasin,
|
||||
"Peachpuff" => PeachPuff,
|
||||
"palegoldenrod" => PaleGoldenrod,
|
||||
"khaki" => Khaki,
|
||||
"darkkhaki" => DarkKhaki,
|
||||
"lavender" => Lavender,
|
||||
"thistle" => Thistle,
|
||||
"plum" => Plum,
|
||||
"violet" => Violet,
|
||||
"orchid" => Orchid,
|
||||
"fuchsia" => Fuchsia,
|
||||
"magenta" => Magenta,
|
||||
"mediumorchid" => MediumOrchid,
|
||||
"mediumpurple" => MediumPurple,
|
||||
"rebeccapurple" => RebeccaPurple,
|
||||
"blueviolet" => BlueViolet,
|
||||
"darkviolet" => DarkViolet,
|
||||
"darkorchid" => DarkOrchid,
|
||||
"darkmagenta" => DarkMagenta,
|
||||
"purple" => Purple,
|
||||
"indigo" => Indigo,
|
||||
"slateblue" => SlateBlue,
|
||||
"darkslateblue" => DarkSlateBlue,
|
||||
"mediumslateblue" => MediumSlateBlue,
|
||||
"greenyellow" => GreenYellow,
|
||||
"chartreuse" => Chartreuse,
|
||||
"lawngreen" => LawnGreen,
|
||||
"lime" => Lime,
|
||||
"limegreen" => LimeGreen,
|
||||
"palegreen" => PaleGreen,
|
||||
"lightgreen" => LightGreen,
|
||||
"mediumspringgreen" => MediumSpringGreen,
|
||||
"springgreen" => SpringGreen,
|
||||
"mediumseagreen" => MediumSeaGreen,
|
||||
"seagreen" => SeaGreen,
|
||||
"forestgreen" => ForestGreen,
|
||||
"green" => Green,
|
||||
"darkgreen" => DarkGreen,
|
||||
"yellowgreen" => YellowGreen,
|
||||
"olivedrab" => OliveDrab,
|
||||
"olive" => Olive,
|
||||
"darkolivegreen" => DarkOliveGreen,
|
||||
"mediumaquamarine" => MediumAquamarine,
|
||||
"darkseagreen" => DarkSeaGreen,
|
||||
"lightseagreen" => LightSeaGreen,
|
||||
"darkcyan" => DarkCyan,
|
||||
"teal" => Teal,
|
||||
"aqua" => Aqua,
|
||||
"cyan" => Cyan,
|
||||
"lightcyan" => LightCyan,
|
||||
"paleturquoise" => PaleTurquoise,
|
||||
"aquamarine" => Aquamarine,
|
||||
"turquoise" => Turquoise,
|
||||
"mediumturquoise" => MediumTurquoise,
|
||||
"darkturquoise" => DarkTurquoise,
|
||||
"cadetblue" => CadetBlue,
|
||||
"steelblue" => SteelBlue,
|
||||
"lightsteelblue" => LightSteelBlue,
|
||||
"powderblue" => PowderBlue,
|
||||
"lightblue" => LightBlue,
|
||||
"skyblue" => SkyBlue,
|
||||
"lightskyblue" => LightSkyBlue,
|
||||
"deepskyblue" => DeepSkyBlue,
|
||||
"dodgerblue" => DodgerBlue,
|
||||
"cornflowerblue" => CornflowerBlue,
|
||||
"royalblue" => RoyalBlue,
|
||||
"blue" => Blue,
|
||||
"mediumblue" => MediumBlue,
|
||||
"darkblue" => DarkBlue,
|
||||
"navy" => Navy,
|
||||
"midnightblue" => MidnightBlue,
|
||||
"cornsilk" => Cornsilk,
|
||||
"blanchedalmond" => BlanchedAlmond,
|
||||
"bisque" => Bisque,
|
||||
"navajowhite" => NavajoWhite,
|
||||
"wheat" => Wheat,
|
||||
"burlywood" => BurlyWood,
|
||||
"tan" => Tan,
|
||||
"rosybrown" => RosyBrown,
|
||||
"sandybrown" => SandyBrown,
|
||||
"goldenrod" => Goldenrod,
|
||||
"darkgoldenrod" => DarkGoldenrod,
|
||||
"peru" => Peru,
|
||||
"chocolate" => Chocolate,
|
||||
"saddlebrown" => SaddleBrown,
|
||||
"sienna" => Sienna,
|
||||
"brown" => Brown,
|
||||
"maroon" => Maroon,
|
||||
"white" => White,
|
||||
"snow" => Snow,
|
||||
"honeydew" => HoneyDew,
|
||||
"mintcream" => MintCream,
|
||||
"azure" => Azure,
|
||||
"aliceblue" => AliceBlue,
|
||||
"ghostwhite" => GhostWhite,
|
||||
"whitesmoke" => WhiteSmoke,
|
||||
"seashell" => SeaShell,
|
||||
"beige" => Beige,
|
||||
"oldlace" => OldLace,
|
||||
"floralwhite" => FloralWhite,
|
||||
"ivory" => Ivory,
|
||||
"antiquewhite" => AntiqueWhite,
|
||||
"linen" => Linen,
|
||||
"lavenderblush" => LavenderBlush,
|
||||
"mistyrose" => MistyRose,
|
||||
"gainsboro" => Gainsboro,
|
||||
"lightgray" => LightGray,
|
||||
"silver" => Silver,
|
||||
"darkgray" => DarkGray,
|
||||
"gray" => Gray,
|
||||
"dimgray" => DimGray,
|
||||
"lightslategray" => LightSlateGray,
|
||||
"slategray" => SlateGray,
|
||||
"darkslategray" => DarkSlateGray,
|
||||
"black" => Black,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Color {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
use Color::*;
|
||||
match self {
|
||||
HexRGB(r, g, b) => write!(f, "#{:02x}{:02x}{:02x}", r, g, b),
|
||||
HexRGBA(r, g, b, a) => write!(f, "#{:02x}{:02x}{:02x}{:02x}", r, g, b, a),
|
||||
HSL(h, s, l) => write!(f, "hsl({}, {}%, {}%)", h, s, l),
|
||||
HSLA(h, s, l, a) => write!(f, "hsla({}, {}%, {}%, {})", h, s, l, a),
|
||||
IndianRed => write!(f, "indianred"),
|
||||
LightCoral => write!(f, "lightcoral"),
|
||||
Salmon => write!(f, "salmon"),
|
||||
DarkSalmon => write!(f, "darksalmon"),
|
||||
LightSalmon => write!(f, "lightsalmon"),
|
||||
Crimson => write!(f, "crimson"),
|
||||
Red => write!(f, "red"),
|
||||
FireBrick => write!(f, "firebrick"),
|
||||
DarkRed => write!(f, "darkred"),
|
||||
Pink => write!(f, "pink"),
|
||||
LightPink => write!(f, "lightpink"),
|
||||
HotPink => write!(f, "hotpink"),
|
||||
DeepPink => write!(f, "deeppink"),
|
||||
MediumVioletRed => write!(f, "mediumvioletred"),
|
||||
PaleVioletRed => write!(f, "palevioletred"),
|
||||
Coral => write!(f, "coral"),
|
||||
Tomato => write!(f, "tomato"),
|
||||
OrangeRed => write!(f, "orangered"),
|
||||
DarkOrange => write!(f, "darkorange"),
|
||||
Orange => write!(f, "orange"),
|
||||
Gold => write!(f, "gold"),
|
||||
Yellow => write!(f, "yellow"),
|
||||
LightYellow => write!(f, "lightyellow"),
|
||||
LemonChiffon => write!(f, "lemonchiffon"),
|
||||
LightGoldenrodYellow => write!(f, "lightgoldenrodyellow"),
|
||||
PapayaWhip => write!(f, "papayawhip"),
|
||||
Moccasin => write!(f, "Moccasin"),
|
||||
PeachPuff => write!(f, "Peachpuff"),
|
||||
PaleGoldenrod => write!(f, "palegoldenrod"),
|
||||
Khaki => write!(f, "khaki"),
|
||||
DarkKhaki => write!(f, "darkkhaki"),
|
||||
Lavender => write!(f, "lavender"),
|
||||
Thistle => write!(f, "thistle"),
|
||||
Plum => write!(f, "plum"),
|
||||
Violet => write!(f, "violet"),
|
||||
Orchid => write!(f, "orchid"),
|
||||
Fuchsia => write!(f, "fuchsia"),
|
||||
Magenta => write!(f, "magenta"),
|
||||
MediumOrchid => write!(f, "mediumorchid"),
|
||||
MediumPurple => write!(f, "mediumpurple"),
|
||||
RebeccaPurple => write!(f, "rebeccapurple"),
|
||||
BlueViolet => write!(f, "blueviolet"),
|
||||
DarkViolet => write!(f, "darkviolet"),
|
||||
DarkOrchid => write!(f, "darkorchid"),
|
||||
DarkMagenta => write!(f, "darkmagenta"),
|
||||
Purple => write!(f, "purple"),
|
||||
Indigo => write!(f, "indigo"),
|
||||
SlateBlue => write!(f, "slateblue"),
|
||||
DarkSlateBlue => write!(f, "darkslateblue"),
|
||||
MediumSlateBlue => write!(f, "mediumslateblue"),
|
||||
GreenYellow => write!(f, "greenyellow"),
|
||||
Chartreuse => write!(f, "chartreuse"),
|
||||
LawnGreen => write!(f, "lawngreen"),
|
||||
Lime => write!(f, "lime"),
|
||||
LimeGreen => write!(f, "limegreen"),
|
||||
PaleGreen => write!(f, "palegreen"),
|
||||
LightGreen => write!(f, "lightgreen"),
|
||||
MediumSpringGreen => write!(f, "mediumspringgreen"),
|
||||
SpringGreen => write!(f, "springgreen"),
|
||||
MediumSeaGreen => write!(f, "mediumseagreen"),
|
||||
SeaGreen => write!(f, "seagreen"),
|
||||
ForestGreen => write!(f, "forestgreen"),
|
||||
Green => write!(f, "green"),
|
||||
DarkGreen => write!(f, "darkgreen"),
|
||||
YellowGreen => write!(f, "yellowgreen"),
|
||||
OliveDrab => write!(f, "olivedrab"),
|
||||
Olive => write!(f, "olive"),
|
||||
DarkOliveGreen => write!(f, "darkolivegreen"),
|
||||
MediumAquamarine => write!(f, "mediumaquamarine"),
|
||||
DarkSeaGreen => write!(f, "darkseagreen"),
|
||||
LightSeaGreen => write!(f, "lightseagreen"),
|
||||
DarkCyan => write!(f, "darkcyan"),
|
||||
Teal => write!(f, "teal"),
|
||||
Aqua => write!(f, "aqua"),
|
||||
Cyan => write!(f, "cyan"),
|
||||
LightCyan => write!(f, "lightcyan"),
|
||||
PaleTurquoise => write!(f, "paleturquoise"),
|
||||
Aquamarine => write!(f, "aquamarine"),
|
||||
Turquoise => write!(f, "turquoise"),
|
||||
MediumTurquoise => write!(f, "mediumturquoise"),
|
||||
DarkTurquoise => write!(f, "darkturquoise"),
|
||||
CadetBlue => write!(f, "cadetblue"),
|
||||
SteelBlue => write!(f, "steelblue"),
|
||||
LightSteelBlue => write!(f, "lightsteelblue"),
|
||||
PowderBlue => write!(f, "powderblue"),
|
||||
LightBlue => write!(f, "lightblue"),
|
||||
SkyBlue => write!(f, "skyblue"),
|
||||
LightSkyBlue => write!(f, "lightskyblue"),
|
||||
DeepSkyBlue => write!(f, "deepskyblue"),
|
||||
DodgerBlue => write!(f, "dodgerblue"),
|
||||
CornflowerBlue => write!(f, "cornflowerblue"),
|
||||
RoyalBlue => write!(f, "royalblue"),
|
||||
Blue => write!(f, "blue"),
|
||||
MediumBlue => write!(f, "mediumblue"),
|
||||
DarkBlue => write!(f, "darkblue"),
|
||||
Navy => write!(f, "navy"),
|
||||
MidnightBlue => write!(f, "midnightblue"),
|
||||
Cornsilk => write!(f, "cornsilk"),
|
||||
BlanchedAlmond => write!(f, "blanchedalmond"),
|
||||
Bisque => write!(f, "bisque"),
|
||||
NavajoWhite => write!(f, "navajowhite"),
|
||||
Wheat => write!(f, "wheat"),
|
||||
BurlyWood => write!(f, "burlywood"),
|
||||
Tan => write!(f, "tan"),
|
||||
RosyBrown => write!(f, "rosybrown"),
|
||||
SandyBrown => write!(f, "sandybrown"),
|
||||
Goldenrod => write!(f, "goldenrod"),
|
||||
DarkGoldenrod => write!(f, "darkgoldenrod"),
|
||||
Peru => write!(f, "peru"),
|
||||
Chocolate => write!(f, "chocolate"),
|
||||
SaddleBrown => write!(f, "saddlebrown"),
|
||||
Sienna => write!(f, "sienna"),
|
||||
Brown => write!(f, "brown"),
|
||||
Maroon => write!(f, "maroon"),
|
||||
White => write!(f, "white"),
|
||||
Snow => write!(f, "snow"),
|
||||
HoneyDew => write!(f, "honeydew"),
|
||||
MintCream => write!(f, "mintcream"),
|
||||
Azure => write!(f, "azure"),
|
||||
AliceBlue => write!(f, "aliceblue"),
|
||||
GhostWhite => write!(f, "ghostwhite"),
|
||||
WhiteSmoke => write!(f, "whitesmoke"),
|
||||
SeaShell => write!(f, "seashell"),
|
||||
Beige => write!(f, "beige"),
|
||||
OldLace => write!(f, "oldlace"),
|
||||
FloralWhite => write!(f, "floralwhite"),
|
||||
Ivory => write!(f, "ivory"),
|
||||
AntiqueWhite => write!(f, "antiquewhite"),
|
||||
Linen => write!(f, "linen"),
|
||||
LavenderBlush => write!(f, "lavenderblush"),
|
||||
MistyRose => write!(f, "mistyrose"),
|
||||
Gainsboro => write!(f, "gainsboro"),
|
||||
LightGray => write!(f, "lightgray"),
|
||||
Silver => write!(f, "silver"),
|
||||
DarkGray => write!(f, "darkgray"),
|
||||
Gray => write!(f, "gray"),
|
||||
DimGray => write!(f, "dimgray"),
|
||||
LightSlateGray => write!(f, "lightslategray"),
|
||||
SlateGray => write!(f, "slategray"),
|
||||
DarkSlateGray => write!(f, "darkslategray"),
|
||||
Black => write!(f, "black"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn hsl_to_rgb(h: f64, s: f64, l: f64) -> (f64, f64, f64) {
|
||||
debug_assert!(h >= 0.0 && h < 360.0);
|
||||
debug_assert!(s >= 0.0 && s <= 1.0);
|
||||
debug_assert!(l >= 0.0 && l <= 1.0);
|
||||
let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
|
||||
let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs());
|
||||
let m = l - c * 0.5;
|
||||
let (rp, gp, bp) = if h < 60.0 {
|
||||
(c, x, 0.0)
|
||||
} else if h < 120.0 {
|
||||
(x, c, 0.0)
|
||||
} else if h < 180.0 {
|
||||
(0.0, c, x)
|
||||
} else if h < 240.0 {
|
||||
(0.0, x, c)
|
||||
} else if h < 300.0 {
|
||||
(x, 0.0, c)
|
||||
} else {
|
||||
(c, 0.0, x)
|
||||
};
|
||||
(rp + m, gp + m, bp + m)
|
||||
}
|
||||
|
||||
pub fn parse_hex(hex: &str) -> Option<Color> {
|
||||
match hex.len() {
|
||||
3 => {
|
||||
let r = u8::from_str_radix(hex.get(0..1)?, 16).ok()?;
|
||||
let g = u8::from_str_radix(hex.get(1..2)?, 16).ok()?;
|
||||
let b = u8::from_str_radix(hex.get(2..3)?, 16).ok()?;
|
||||
// #fff is equivalent to #ffffff
|
||||
Some(Color::HexRGB(r << 4 | r, g << 4 | g, b << 4 | b))
|
||||
}
|
||||
6 => {
|
||||
let r = u8::from_str_radix(hex.get(0..2)?, 16).ok()?;
|
||||
let g = u8::from_str_radix(hex.get(2..4)?, 16).ok()?;
|
||||
let b = u8::from_str_radix(hex.get(4..6)?, 16).ok()?;
|
||||
Some(Color::HexRGB(r, g, b))
|
||||
}
|
||||
8 => {
|
||||
let r = u8::from_str_radix(hex.get(0..2)?, 16).ok()?;
|
||||
let g = u8::from_str_radix(hex.get(2..4)?, 16).ok()?;
|
||||
let b = u8::from_str_radix(hex.get(4..6)?, 16).ok()?;
|
||||
let a = u8::from_str_radix(hex.get(6..8)?, 16).ok()?;
|
||||
Some(Color::HexRGBA(r, g, b, a))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_color_convert() {
|
||||
let color = Color::HSL(60.0, 0.0, 100.0);
|
||||
assert_eq!(color.to_rgb(), Color::HexRGB(255, 255, 255));
|
||||
let color = Color::HSL(0.0, 100.0, 50.0);
|
||||
assert_eq!(color.to_rgb(), Color::HexRGB(255, 0, 0));
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,502 +0,0 @@
|
||||
//! Parse the various css types from strings directly (avoid pulling in syn if working at runtime)
|
||||
//!
|
||||
//! Differences to spec:
|
||||
//! - Exponential floats are not supported for now.
|
||||
use std::{char, fmt, iter};
|
||||
|
||||
const REPLACEMENT_CHAR: char = '�';
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
#[non_exhaustive] // Don't allow user to create
|
||||
pub struct Span {
|
||||
/// Inclusive
|
||||
start: usize,
|
||||
/// Exclusive
|
||||
end: usize,
|
||||
}
|
||||
|
||||
impl Span {
|
||||
fn new(start: usize, end: usize) -> Self {
|
||||
assert!(end > start, "end must be greater than start");
|
||||
Span { start, end }
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.end - self.start
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InvalidChar {
|
||||
ch: char,
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
impl fmt::Display for InvalidChar {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"invalid character `{}` found at position {}",
|
||||
self.ch.escape_debug(),
|
||||
self.pos
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Lexer<'src> {
|
||||
src: &'src str,
|
||||
cursor: usize,
|
||||
}
|
||||
|
||||
impl<'src> Lexer<'src> {
|
||||
pub fn new(src: &'src str) -> Result<Lexer<'src>, InvalidChar> {
|
||||
// Check that the user has already replaced characters as specified at
|
||||
// https://www.w3.org/TR/css-syntax-3/#input-preprocessing
|
||||
for (pos, ch) in src.char_indices() {
|
||||
if ch == '\r' || ch == '\u{d}' || ch == '\0' {
|
||||
return Err(InvalidChar { ch, pos });
|
||||
}
|
||||
}
|
||||
Ok(Lexer { src, cursor: 0 })
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.src.len()
|
||||
}
|
||||
|
||||
fn remaining(&self) -> usize {
|
||||
self.src.len() - self.cursor
|
||||
}
|
||||
|
||||
pub fn next_token(&mut self) -> Option<Token> {
|
||||
match self.peek() {
|
||||
Some(token) => {
|
||||
self.consume(&token);
|
||||
Some(token)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn peek(&self) -> Option<Token> {
|
||||
// https://www.w3.org/TR/css-syntax-3/#tokenizer-definitions
|
||||
if let Some(comment) = self.comment() {
|
||||
return Some(comment);
|
||||
}
|
||||
if let Some(tok) = self.whitespace() {
|
||||
return Some(tok);
|
||||
}
|
||||
if let Some(tok) = self.string() {
|
||||
return Some(tok);
|
||||
}
|
||||
match self.chars().next() {
|
||||
Some(other) => Some(Token::new(
|
||||
TokenKind::Error,
|
||||
Span::new(self.cursor, self.cursor + other.len_utf8()),
|
||||
)),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn peek_n(&self, n: usize) -> Option<Token> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
todo!() //self.peek().is_none()
|
||||
}
|
||||
|
||||
pub fn resolve_span(&self, span: Span) -> &'src str {
|
||||
if span.end > self.len() {
|
||||
panic!("End of requested span is past the end of the source");
|
||||
}
|
||||
&self.src[span.start..span.end]
|
||||
}
|
||||
|
||||
/// Create another independent lexer at the given start point
|
||||
fn fork(&self) -> Lexer {
|
||||
Lexer {
|
||||
src: self.src,
|
||||
cursor: self.cursor,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn consume(&mut self, tok: &Token) {
|
||||
assert!(
|
||||
tok.len() <= self.remaining(),
|
||||
"trying to consume a token that would be bigger \
|
||||
than all remaining text"
|
||||
);
|
||||
self.cursor += tok.len();
|
||||
}
|
||||
|
||||
/// Resolve a position from cursor to position from start of src
|
||||
fn resolve_pos(&self, pos: usize) -> usize {
|
||||
self.cursor + pos
|
||||
}
|
||||
|
||||
/// Create a span from the current position with the given length
|
||||
fn span(&self, len: usize) -> Span {
|
||||
debug_assert!(self.cursor + len <= self.len());
|
||||
Span::new(self.cursor, self.cursor + len)
|
||||
}
|
||||
|
||||
/// Create a span from the current position to the end
|
||||
fn span_to_end(&self) -> Span {
|
||||
Span::new(self.cursor, self.len())
|
||||
}
|
||||
|
||||
/// Iterate over the remaining chars of the input
|
||||
fn chars(&self) -> std::str::Chars {
|
||||
self.src[self.cursor..].chars()
|
||||
}
|
||||
|
||||
/// Iterate over the remaining chars of the input
|
||||
fn char_indices(&self) -> std::str::CharIndices {
|
||||
self.src[self.cursor..].char_indices()
|
||||
}
|
||||
|
||||
/// Parse a comment
|
||||
fn comment(&self) -> Option<Token> {
|
||||
let mut ch_iter = self.char_indices().peekable();
|
||||
if let Some((_, '/')) = ch_iter.next() {
|
||||
if let Some((_, '*')) = ch_iter.next() {
|
||||
loop {
|
||||
match ch_iter.next() {
|
||||
Some((_, '*')) => {
|
||||
if let Some((idx, '/')) = ch_iter.peek() {
|
||||
return Some(Token {
|
||||
kind: TokenKind::Comment,
|
||||
span: self.span(*idx + '/'.len_utf8()),
|
||||
});
|
||||
}
|
||||
}
|
||||
None => {
|
||||
return Some(Token::new(
|
||||
TokenKind::UnclosedComment,
|
||||
self.span_to_end(),
|
||||
));
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Parse whitespace
|
||||
fn whitespace(&self) -> Option<Token> {
|
||||
let mut ch_iter = self.chars();
|
||||
let mut len = match ch_iter.next() {
|
||||
Some(ch) if ch.is_ascii_whitespace() => ch.len_utf8(),
|
||||
_ => return None,
|
||||
};
|
||||
loop {
|
||||
match ch_iter.next() {
|
||||
Some(ch) if ch.is_ascii_whitespace() => len += ch.len_utf8(),
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
Some(Token {
|
||||
kind: TokenKind::Whitespace,
|
||||
span: self.span(len),
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse either a single or double quoted string
|
||||
fn string(&self) -> Option<Token> {
|
||||
let mut ch_iter = self.char_indices().fuse().peekable();
|
||||
let delim = match ch_iter.next() {
|
||||
Some((_, '"')) => '"',
|
||||
Some((_, '\'')) => '\'',
|
||||
_ => return None,
|
||||
};
|
||||
let mut decoded_string = String::new();
|
||||
loop {
|
||||
match ch_iter.next() {
|
||||
Some((end, ch)) if ch == delim => {
|
||||
return Some(Token {
|
||||
kind: TokenKind::String(decoded_string),
|
||||
span: self.span(end + 1), // '"'.len_utf8() == 1
|
||||
});
|
||||
}
|
||||
Some((end, '\n')) => {
|
||||
return Some(Token {
|
||||
kind: TokenKind::BadString(decoded_string),
|
||||
span: self.span(end + 1), // '\n'.len_utf8() == 1
|
||||
});
|
||||
}
|
||||
Some((_, '\\')) => match ch_iter.peek() {
|
||||
Some((_, ch)) => {
|
||||
if *ch == '\n' {
|
||||
// do nothing - skip the backslash and newline.
|
||||
ch_iter.next().unwrap();
|
||||
} else if let Some(decoded_ch) = unescape(&mut ch_iter) {
|
||||
decoded_string.push(decoded_ch);
|
||||
} else {
|
||||
decoded_string.push(ch_iter.next().unwrap().1);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// The spec says not to add the last '\'.
|
||||
// a bad string will be returned on next pass
|
||||
ch_iter.next().unwrap();
|
||||
}
|
||||
},
|
||||
Some((_, ch)) => decoded_string.push(ch),
|
||||
None => {
|
||||
return Some(Token {
|
||||
kind: TokenKind::BadString(decoded_string),
|
||||
span: self.span_to_end(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
fn hash(&self) -> Option<Token> {
|
||||
let mut iter = self.char_indices();
|
||||
match iter.next() {
|
||||
Some((_, '#')) => (),
|
||||
None => return None,
|
||||
};
|
||||
match iter.next() {
|
||||
Some((_, '\\')) => {}
|
||||
_ => Some(Token {
|
||||
kind: TokenKind::Delim('#'),
|
||||
span: self.span(1),
|
||||
}),
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
impl<'src> Iterator for Lexer<'src> {
|
||||
type Item = Token;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.next_token()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub struct Token {
|
||||
pub kind: TokenKind,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
impl Token {
|
||||
fn new(kind: TokenKind, span: Span) -> Self {
|
||||
Token { kind, span }
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.span.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum TokenKind {
|
||||
Ident,
|
||||
Function,
|
||||
At,
|
||||
Hash,
|
||||
String(String),
|
||||
BadString(String),
|
||||
Url,
|
||||
BadUrl,
|
||||
Delim(char),
|
||||
Number,
|
||||
Percentage,
|
||||
Dimension,
|
||||
Whitespace,
|
||||
/// <!--
|
||||
CDO,
|
||||
/// -->
|
||||
CDC,
|
||||
/// :
|
||||
Colon,
|
||||
/// ;
|
||||
Semicolon,
|
||||
/// ,
|
||||
Comma,
|
||||
/// [
|
||||
LBracket,
|
||||
/// ]
|
||||
RBracket,
|
||||
/// (
|
||||
LParen,
|
||||
/// )
|
||||
RParen,
|
||||
/// {
|
||||
LBrace,
|
||||
/// }
|
||||
RBrace,
|
||||
Comment,
|
||||
UnclosedComment,
|
||||
/// Could not parse the next token
|
||||
Error,
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
/// Hex to char (up to 6 characters, e.g. "ffffff").
|
||||
///
|
||||
/// For example `"5c" => '\'`. Returns None if first char is not hex. Consumes the hex values.
|
||||
fn unescape(input: &mut iter::Peekable<impl Iterator<Item = (usize, char)>>) -> Option<char> {
|
||||
fn hex_acc(acc: &mut u32, next: char) {
|
||||
debug_assert!(*acc & 0xf0000000 == 0); // make sure we don't overflow
|
||||
(*acc) = (*acc << 4) + next.to_digit(16).unwrap()
|
||||
}
|
||||
|
||||
let (_, ch) = match input.peek() {
|
||||
Some((idx, ch)) if ch.is_ascii_hexdigit() => input.next().unwrap(),
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let mut acc = 0;
|
||||
let mut count = 0;
|
||||
hex_acc(&mut acc, ch);
|
||||
|
||||
// Here we use that the length of all valid hexdigits in utf8 is 1.
|
||||
while count < 5
|
||||
&& input
|
||||
.peek()
|
||||
.map(|(_, ch)| ch.is_ascii_hexdigit())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let ch = input.next().unwrap().1;
|
||||
hex_acc(&mut acc, ch);
|
||||
count += 1;
|
||||
}
|
||||
|
||||
// consume a whitespace char if it's there
|
||||
if input
|
||||
.peek()
|
||||
.map(|(_, ch)| ch.is_ascii_whitespace())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
input.next().unwrap();
|
||||
}
|
||||
|
||||
// maybe we could just directly use `char::from_u32(acc).unwrap_or(REPLACEMENT_CHAR)`
|
||||
// null, surrogate, or too big
|
||||
Some(
|
||||
if acc == 0 || (acc >= 0xd800 && acc < 0xe000) || acc >= 0x110000 {
|
||||
REPLACEMENT_CHAR
|
||||
} else {
|
||||
char::from_u32(acc).unwrap() // there should be no other invalid chars.
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{Lexer, Span, Token, TokenKind};
|
||||
|
||||
#[test]
|
||||
fn comment() {
|
||||
println!();
|
||||
let mut input = Lexer::new("/* a valid comment */").unwrap();
|
||||
match input.next_token() {
|
||||
Some(Token {
|
||||
kind: TokenKind::Comment,
|
||||
span,
|
||||
}) => {
|
||||
assert_eq!(
|
||||
input.resolve_span(span),
|
||||
"/* a valid comment */".to_string()
|
||||
);
|
||||
assert_eq!(span.len(), 21);
|
||||
}
|
||||
_ => panic!("not a comment"),
|
||||
};
|
||||
|
||||
let mut input = Lexer::new("/* a comment").unwrap();
|
||||
match input.next_token() {
|
||||
Some(Token {
|
||||
kind: TokenKind::UnclosedComment,
|
||||
span,
|
||||
}) => {
|
||||
assert_eq!(input.resolve_span(span), "/* a comment".to_string());
|
||||
assert_eq!(span.len(), 12);
|
||||
}
|
||||
_ => panic!("not a comment"),
|
||||
};
|
||||
|
||||
let mut input = Lexer::new("/!* not a comment").unwrap();
|
||||
match input.next_token() {
|
||||
Some(Token {
|
||||
kind: TokenKind::Error,
|
||||
span,
|
||||
}) => {}
|
||||
_ => panic!("not a comment"),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string() {
|
||||
println!("h");
|
||||
let mut input = Lexer::new("\" a vali\\64\\e9 \\\n string \"").unwrap();
|
||||
match input.next_token() {
|
||||
Some(Token {
|
||||
kind: TokenKind::String(s),
|
||||
span,
|
||||
}) => {
|
||||
assert_eq!(s, " a validé string ".to_string());
|
||||
assert_eq!(span.len(), 26);
|
||||
}
|
||||
_ => panic!("not a string"),
|
||||
};
|
||||
|
||||
let mut input = Lexer::new("' a valid string '").unwrap();
|
||||
match input.next_token() {
|
||||
Some(Token {
|
||||
kind: TokenKind::String(s),
|
||||
span,
|
||||
}) => {
|
||||
assert_eq!(s, " a valid string ".to_string());
|
||||
assert_eq!(span.len(), 18);
|
||||
}
|
||||
_ => panic!("not a string"),
|
||||
};
|
||||
|
||||
let mut input = Lexer::new("\" a string").unwrap();
|
||||
match input.next_token() {
|
||||
Some(Token {
|
||||
kind: TokenKind::BadString(s),
|
||||
span,
|
||||
}) => {
|
||||
assert_eq!(s, " a string".to_string());
|
||||
assert_eq!(span.len(), 10);
|
||||
}
|
||||
_ => panic!("not a string"),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn whitespace() {
|
||||
println!();
|
||||
let mut input = Lexer::new("\n\t ").unwrap();
|
||||
match input.next_token() {
|
||||
Some(Token {
|
||||
kind: TokenKind::Whitespace,
|
||||
span,
|
||||
}) => {
|
||||
assert_eq!(input.resolve_span(span), "\n\t ".to_string());
|
||||
assert_eq!(span.len(), 3);
|
||||
}
|
||||
_ => panic!("not a string"),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escape() {
|
||||
let mut iter = "e9".char_indices().peekable();
|
||||
assert_eq!(super::unescape(&mut iter), Some('é'));
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod lexer;
|
||||
File diff suppressed because it is too large.
Load diff
@@ -22,15 +22,13 @@ longest-increasing-subsequence = "0.1.0"
|
||||
# internall used
|
||||
log = { version = "0.4", features = ["release_max_level_off"] }
|
||||
|
||||
futures-util = { version = "0.3.15", default-features = false, features = [
|
||||
"std",
|
||||
] }
|
||||
futures-util = { version = "0.3.15", default-features = false }
|
||||
|
||||
smallvec = "1.6.1"
|
||||
|
||||
slab = "0.4.3"
|
||||
|
||||
futures-channel = "0.3.16"
|
||||
futures-channel = "0.3.18"
|
||||
|
||||
# used for noderefs
|
||||
once_cell = "1.8.0"
|
||||
@@ -39,6 +37,8 @@ indexmap = "1.7.0"
|
||||
|
||||
# Serialize the Edits for use in Webview/Liveview instances
|
||||
serde = { version = "1", features = ["derive"], optional = true }
|
||||
|
||||
# todo: I want to get rid of this
|
||||
backtrace = "0.3.63"
|
||||
|
||||
[dev-dependencies]
|
||||
@@ -49,8 +49,7 @@ rand = { version = "0.8.4", features = ["small_rng"] }
|
||||
simple_logger = "1.13.0"
|
||||
dioxus-core-macro = { path = "../core-macro", version = "0.1.2" }
|
||||
dioxus-hooks = { path = "../hooks" }
|
||||
# async-std = { version = "1.9.0", features = ["attributes"] }
|
||||
# criterion = "0.3.5"
|
||||
criterion = "0.3.5"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
@@ -64,3 +63,7 @@ harness = false
|
||||
[[bench]]
|
||||
name = "jsframework"
|
||||
harness = false
|
||||
|
||||
[[example]]
|
||||
name = "rows"
|
||||
path = "./examples/rows.rs"
|
||||
@@ -56,3 +56,23 @@ There's a few invariants that are very important:
|
||||
|
||||
- References to `ScopeInner` and `Props` passed into components are *always* valid for as long as the component exists. Even if the scope backing is resized to fit more scopes, the scope has to stay the same place in memory.
|
||||
|
||||
|
||||
|
||||
## Suspense
|
||||
|
||||
Suspense is done through combinators on values.
|
||||
|
||||
```rust
|
||||
let name = get_name(cx).suspend();
|
||||
|
||||
rsx!(
|
||||
div {
|
||||
{name}
|
||||
div {
|
||||
div {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
```
|
||||
@@ -201,3 +201,8 @@ Open questions:
|
||||
- react says no - they are continuous
|
||||
- but if we received both - then we don't need to diff, do we? run as many as we can and then finally diff?
|
||||
|
||||
|
||||
|
||||
## Render functions
|
||||
|
||||
One main issue of components is type eras
|
||||
@@ -19,7 +19,6 @@ use dioxus_core_macro::*;
|
||||
use dioxus_html as dioxus_elements;
|
||||
use rand::prelude::*;
|
||||
|
||||
fn main() {}
|
||||
criterion_group!(mbenches, create_rows);
|
||||
criterion_main!(mbenches);
|
||||
|
||||
@@ -28,18 +27,14 @@ fn create_rows(c: &mut Criterion) {
|
||||
let mut rng = SmallRng::from_entropy();
|
||||
let rows = (0..10_000_usize).map(|f| {
|
||||
let label = Label::new(&mut rng);
|
||||
rsx! {
|
||||
Row {
|
||||
row_id: f,
|
||||
label: label
|
||||
}
|
||||
}
|
||||
rsx!(Row {
|
||||
row_id: f,
|
||||
label: label
|
||||
})
|
||||
});
|
||||
cx.render(rsx! {
|
||||
table {
|
||||
tbody {
|
||||
{rows}
|
||||
}
|
||||
rsx!(cx, table {
|
||||
tbody {
|
||||
{rows}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
#![allow(non_snake_case, non_upper_case_globals)]
|
||||
//! This benchmark tests just the overhead of Dioxus itself.
|
||||
//!
|
||||
//! For the JS Framework Benchmark, both the framework and the browser is benchmarked together. Dioxus prepares changes
|
||||
//! to be made, but the change application phase will be just as performant as the vanilla wasm_bindgen code. In essence,
|
||||
//! we are measuring the overhead of Dioxus, not the performance of the "apply" phase.
|
||||
//!
|
||||
//! On my MBP 2019:
|
||||
//! - Dioxus takes 3ms to create 1_000 rows
|
||||
//! - Dioxus takes 30ms to create 10_000 rows
|
||||
//!
|
||||
//! As pure "overhead", these are amazing good numbers, mostly slowed down by hitting the global allocator.
|
||||
//! These numbers don't represent Dioxus with the heuristic engine installed, so I assume it'll be even faster.
|
||||
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use dioxus_core as dioxus;
|
||||
use dioxus_core::prelude::*;
|
||||
use dioxus_core_macro::*;
|
||||
use dioxus_html as dioxus_elements;
|
||||
use rand::prelude::*;
|
||||
|
||||
fn main() {
|
||||
static App: FC<()> = |cx, _| {
|
||||
let mut rng = SmallRng::from_entropy();
|
||||
let rows = (0..10_000_usize).map(|f| {
|
||||
let label = Label::new(&mut rng);
|
||||
rsx! {
|
||||
Row {
|
||||
row_id: f,
|
||||
label: label
|
||||
}
|
||||
}
|
||||
});
|
||||
cx.render(rsx! {
|
||||
table {
|
||||
tbody {
|
||||
{rows}
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let mut dom = VirtualDom::new(App);
|
||||
let g = dom.rebuild();
|
||||
assert!(g.edits.len() > 1);
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Props)]
|
||||
struct RowProps {
|
||||
row_id: usize,
|
||||
label: Label,
|
||||
}
|
||||
fn Row(cx: Context, props: &RowProps) -> Element {
|
||||
let [adj, col, noun] = props.label.0;
|
||||
cx.render(rsx! {
|
||||
tr {
|
||||
td { class:"col-md-1", "{props.row_id}" }
|
||||
td { class:"col-md-1", onclick: move |_| { /* run onselect */ }
|
||||
a { class: "lbl", "{adj}" "{col}" "{noun}" }
|
||||
}
|
||||
td { class: "col-md-1"
|
||||
a { class: "remove", onclick: move |_| {/* remove */}
|
||||
span { class: "glyphicon glyphicon-remove remove" aria_hidden: "true" }
|
||||
}
|
||||
}
|
||||
td { class: "col-md-6" }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct Label([&'static str; 3]);
|
||||
|
||||
impl Label {
|
||||
fn new(rng: &mut SmallRng) -> Self {
|
||||
Label([
|
||||
ADJECTIVES.choose(rng).unwrap(),
|
||||
COLOURS.choose(rng).unwrap(),
|
||||
NOUNS.choose(rng).unwrap(),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
static ADJECTIVES: &[&str] = &[
|
||||
"pretty",
|
||||
"large",
|
||||
"big",
|
||||
"small",
|
||||
"tall",
|
||||
"short",
|
||||
"long",
|
||||
"handsome",
|
||||
"plain",
|
||||
"quaint",
|
||||
"clean",
|
||||
"elegant",
|
||||
"easy",
|
||||
"angry",
|
||||
"crazy",
|
||||
"helpful",
|
||||
"mushy",
|
||||
"odd",
|
||||
"unsightly",
|
||||
"adorable",
|
||||
"important",
|
||||
"inexpensive",
|
||||
"cheap",
|
||||
"expensive",
|
||||
"fancy",
|
||||
];
|
||||
|
||||
static COLOURS: &[&str] = &[
|
||||
"red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black",
|
||||
"orange",
|
||||
];
|
||||
|
||||
static NOUNS: &[&str] = &[
|
||||
"table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger",
|
||||
"pizza", "mouse", "keyboard",
|
||||
];
|
||||
@@ -0,0 +1,412 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg version="1.1" width="1200" height="518" onload="init(evt)" viewBox="0 0 1200 518" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:fg="http://github.com/jonhoo/inferno"><!--Flame graph stack visualization. See https://github.com/brendangregg/FlameGraph for latest version, and http://www.brendangregg.com/flamegraphs.html for examples.--><!--NOTES: --><defs><linearGradient id="background" y1="0" y2="1" x1="0" x2="0"><stop stop-color="#eeeeee" offset="5%"/><stop stop-color="#eeeeb0" offset="95%"/></linearGradient></defs><style type="text/css">
|
||||
text { font-family:"Verdana"; font-size:12px; fill:rgb(0,0,0); }
|
||||
#title { text-anchor:middle; font-size:17px; }
|
||||
#search { opacity:0.1; cursor:pointer; }
|
||||
#search:hover, #search.show { opacity:1; }
|
||||
#subtitle { text-anchor:middle; font-color:rgb(160,160,160); }
|
||||
#unzoom { cursor:pointer; }
|
||||
#frames > *:hover { stroke:black; stroke-width:0.5; cursor:pointer; }
|
||||
.hide { display:none; }
|
||||
.parent { opacity:0.5; }
|
||||
</style><script type="text/ecmascript"><![CDATA[var nametype = 'Function:';
|
||||
var fontsize = 12;
|
||||
var fontwidth = 0.59;
|
||||
var xpad = 10;
|
||||
var inverted = false;
|
||||
var searchcolor = 'rgb(230,0,230)';
|
||||
var fluiddrawing = true;
|
||||
var truncate_text_right = false;]]><![CDATA["use strict";
|
||||
var details, searchbtn, unzoombtn, matchedtxt, svg, searching, frames;
|
||||
function init(evt) {
|
||||
details = document.getElementById("details").firstChild;
|
||||
searchbtn = document.getElementById("search");
|
||||
unzoombtn = document.getElementById("unzoom");
|
||||
matchedtxt = document.getElementById("matched");
|
||||
svg = document.getElementsByTagName("svg")[0];
|
||||
frames = document.getElementById("frames");
|
||||
total_samples = parseInt(frames.attributes.total_samples.value);
|
||||
searching = 0;
|
||||
|
||||
// Use GET parameters to restore a flamegraph's state.
|
||||
var restore_state = function() {
|
||||
var params = get_params();
|
||||
if (params.x && params.y)
|
||||
zoom(find_group(document.querySelector('[*|x="' + params.x + '"][y="' + params.y + '"]')));
|
||||
if (params.s)
|
||||
search(params.s);
|
||||
};
|
||||
|
||||
if (fluiddrawing) {
|
||||
// Make width dynamic so the SVG fits its parent's width.
|
||||
svg.removeAttribute("width");
|
||||
// Edge requires us to have a viewBox that gets updated with size changes.
|
||||
var isEdge = /Edge\/\d./i.test(navigator.userAgent);
|
||||
if (!isEdge) {
|
||||
svg.removeAttribute("viewBox");
|
||||
}
|
||||
var update_for_width_change = function() {
|
||||
if (isEdge) {
|
||||
svg.attributes.viewBox.value = "0 0 " + svg.width.baseVal.value + " " + svg.height.baseVal.value;
|
||||
}
|
||||
|
||||
// Keep consistent padding on left and right of frames container.
|
||||
frames.attributes.width.value = svg.width.baseVal.value - xpad * 2;
|
||||
|
||||
// Text truncation needs to be adjusted for the current width.
|
||||
var el = frames.children;
|
||||
for(var i = 0; i < el.length; i++) {
|
||||
update_text(el[i]);
|
||||
}
|
||||
|
||||
// Keep search elements at a fixed distance from right edge.
|
||||
var svgWidth = svg.width.baseVal.value;
|
||||
searchbtn.attributes.x.value = svgWidth - xpad - 100;
|
||||
matchedtxt.attributes.x.value = svgWidth - xpad - 100;
|
||||
};
|
||||
window.addEventListener('resize', function() {
|
||||
update_for_width_change();
|
||||
});
|
||||
// This needs to be done asynchronously for Safari to work.
|
||||
setTimeout(function() {
|
||||
unzoom();
|
||||
update_for_width_change();
|
||||
restore_state();
|
||||
}, 0);
|
||||
} else {
|
||||
restore_state();
|
||||
}
|
||||
}
|
||||
// event listeners
|
||||
window.addEventListener("click", function(e) {
|
||||
var target = find_group(e.target);
|
||||
if (target) {
|
||||
if (target.nodeName == "a") {
|
||||
if (e.ctrlKey === false) return;
|
||||
e.preventDefault();
|
||||
}
|
||||
if (target.classList.contains("parent")) unzoom();
|
||||
zoom(target);
|
||||
|
||||
// set parameters for zoom state
|
||||
var el = target.querySelector("rect");
|
||||
if (el && el.attributes && el.attributes.y && el.attributes["fg:x"]) {
|
||||
var params = get_params()
|
||||
params.x = el.attributes["fg:x"].value;
|
||||
params.y = el.attributes.y.value;
|
||||
history.replaceState(null, null, parse_params(params));
|
||||
}
|
||||
}
|
||||
else if (e.target.id == "unzoom") {
|
||||
unzoom();
|
||||
|
||||
// remove zoom state
|
||||
var params = get_params();
|
||||
if (params.x) delete params.x;
|
||||
if (params.y) delete params.y;
|
||||
history.replaceState(null, null, parse_params(params));
|
||||
}
|
||||
else if (e.target.id == "search") search_prompt();
|
||||
}, false)
|
||||
// mouse-over for info
|
||||
// show
|
||||
window.addEventListener("mouseover", function(e) {
|
||||
var target = find_group(e.target);
|
||||
if (target) details.nodeValue = nametype + " " + g_to_text(target);
|
||||
}, false)
|
||||
// clear
|
||||
window.addEventListener("mouseout", function(e) {
|
||||
var target = find_group(e.target);
|
||||
if (target) details.nodeValue = ' ';
|
||||
}, false)
|
||||
// ctrl-F for search
|
||||
window.addEventListener("keydown",function (e) {
|
||||
if (e.keyCode === 114 || (e.ctrlKey && e.keyCode === 70)) {
|
||||
e.preventDefault();
|
||||
search_prompt();
|
||||
}
|
||||
}, false)
|
||||
// functions
|
||||
function get_params() {
|
||||
var params = {};
|
||||
var paramsarr = window.location.search.substr(1).split('&');
|
||||
for (var i = 0; i < paramsarr.length; ++i) {
|
||||
var tmp = paramsarr[i].split("=");
|
||||
if (!tmp[0] || !tmp[1]) continue;
|
||||
params[tmp[0]] = decodeURIComponent(tmp[1]);
|
||||
}
|
||||
return params;
|
||||
}
|
||||
function parse_params(params) {
|
||||
var uri = "?";
|
||||
for (var key in params) {
|
||||
uri += key + '=' + encodeURIComponent(params[key]) + '&';
|
||||
}
|
||||
if (uri.slice(-1) == "&")
|
||||
uri = uri.substring(0, uri.length - 1);
|
||||
if (uri == '?')
|
||||
uri = window.location.href.split('?')[0];
|
||||
return uri;
|
||||
}
|
||||
function find_child(node, selector) {
|
||||
var children = node.querySelectorAll(selector);
|
||||
if (children.length) return children[0];
|
||||
return;
|
||||
}
|
||||
function find_group(node) {
|
||||
var parent = node.parentElement;
|
||||
if (!parent) return;
|
||||
if (parent.id == "frames") return node;
|
||||
return find_group(parent);
|
||||
}
|
||||
function orig_save(e, attr, val) {
|
||||
if (e.attributes["fg:orig_" + attr] != undefined) return;
|
||||
if (e.attributes[attr] == undefined) return;
|
||||
if (val == undefined) val = e.attributes[attr].value;
|
||||
e.setAttribute("fg:orig_" + attr, val);
|
||||
}
|
||||
function orig_load(e, attr) {
|
||||
if (e.attributes["fg:orig_"+attr] == undefined) return;
|
||||
e.attributes[attr].value = e.attributes["fg:orig_" + attr].value;
|
||||
e.removeAttribute("fg:orig_" + attr);
|
||||
}
|
||||
function g_to_text(e) {
|
||||
var text = find_child(e, "title").firstChild.nodeValue;
|
||||
return (text)
|
||||
}
|
||||
function g_to_func(e) {
|
||||
var func = g_to_text(e);
|
||||
// if there's any manipulation we want to do to the function
|
||||
// name before it's searched, do it here before returning.
|
||||
return (func);
|
||||
}
|
||||
function update_text(e) {
|
||||
var r = find_child(e, "rect");
|
||||
var t = find_child(e, "text");
|
||||
var w = parseFloat(r.attributes.width.value) * frames.attributes.width.value / 100 - 3;
|
||||
var txt = find_child(e, "title").textContent.replace(/\([^(]*\)$/,"");
|
||||
t.attributes.x.value = format_percent((parseFloat(r.attributes.x.value) + (100 * 3 / frames.attributes.width.value)));
|
||||
// Smaller than this size won't fit anything
|
||||
if (w < 2 * fontsize * fontwidth) {
|
||||
t.textContent = "";
|
||||
return;
|
||||
}
|
||||
t.textContent = txt;
|
||||
// Fit in full text width
|
||||
if (/^ *\$/.test(txt) || t.getComputedTextLength() < w)
|
||||
return;
|
||||
if (truncate_text_right) {
|
||||
// Truncate the right side of the text.
|
||||
for (var x = txt.length - 2; x > 0; x--) {
|
||||
if (t.getSubStringLength(0, x + 2) <= w) {
|
||||
t.textContent = txt.substring(0, x) + "..";
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Truncate the left side of the text.
|
||||
for (var x = 2; x < txt.length; x++) {
|
||||
if (t.getSubStringLength(x - 2, txt.length) <= w) {
|
||||
t.textContent = ".." + txt.substring(x, txt.length);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
t.textContent = "";
|
||||
}
|
||||
// zoom
|
||||
function zoom_reset(e) {
|
||||
if (e.tagName == "rect") {
|
||||
e.attributes.x.value = format_percent(100 * parseInt(e.attributes["fg:x"].value) / total_samples);
|
||||
e.attributes.width.value = format_percent(100 * parseInt(e.attributes["fg:w"].value) / total_samples);
|
||||
}
|
||||
if (e.childNodes == undefined) return;
|
||||
for(var i = 0, c = e.childNodes; i < c.length; i++) {
|
||||
zoom_reset(c[i]);
|
||||
}
|
||||
}
|
||||
function zoom_child(e, x, zoomed_width_samples) {
|
||||
if (e.tagName == "text") {
|
||||
var parent_x = parseFloat(find_child(e.parentNode, "rect[x]").attributes.x.value);
|
||||
e.attributes.x.value = format_percent(parent_x + (100 * 3 / frames.attributes.width.value));
|
||||
} else if (e.tagName == "rect") {
|
||||
e.attributes.x.value = format_percent(100 * (parseInt(e.attributes["fg:x"].value) - x) / zoomed_width_samples);
|
||||
e.attributes.width.value = format_percent(100 * parseInt(e.attributes["fg:w"].value) / zoomed_width_samples);
|
||||
}
|
||||
if (e.childNodes == undefined) return;
|
||||
for(var i = 0, c = e.childNodes; i < c.length; i++) {
|
||||
zoom_child(c[i], x, zoomed_width_samples);
|
||||
}
|
||||
}
|
||||
function zoom_parent(e) {
|
||||
if (e.attributes) {
|
||||
if (e.attributes.x != undefined) {
|
||||
e.attributes.x.value = "0.0%";
|
||||
}
|
||||
if (e.attributes.width != undefined) {
|
||||
e.attributes.width.value = "100.0%";
|
||||
}
|
||||
}
|
||||
if (e.childNodes == undefined) return;
|
||||
for(var i = 0, c = e.childNodes; i < c.length; i++) {
|
||||
zoom_parent(c[i]);
|
||||
}
|
||||
}
|
||||
function zoom(node) {
|
||||
var attr = find_child(node, "rect").attributes;
|
||||
var width = parseInt(attr["fg:w"].value);
|
||||
var xmin = parseInt(attr["fg:x"].value);
|
||||
var xmax = xmin + width;
|
||||
var ymin = parseFloat(attr.y.value);
|
||||
unzoombtn.classList.remove("hide");
|
||||
var el = frames.children;
|
||||
for (var i = 0; i < el.length; i++) {
|
||||
var e = el[i];
|
||||
var a = find_child(e, "rect").attributes;
|
||||
var ex = parseInt(a["fg:x"].value);
|
||||
var ew = parseInt(a["fg:w"].value);
|
||||
// Is it an ancestor
|
||||
if (!inverted) {
|
||||
var upstack = parseFloat(a.y.value) > ymin;
|
||||
} else {
|
||||
var upstack = parseFloat(a.y.value) < ymin;
|
||||
}
|
||||
if (upstack) {
|
||||
// Direct ancestor
|
||||
if (ex <= xmin && (ex+ew) >= xmax) {
|
||||
e.classList.add("parent");
|
||||
zoom_parent(e);
|
||||
update_text(e);
|
||||
}
|
||||
// not in current path
|
||||
else
|
||||
e.classList.add("hide");
|
||||
}
|
||||
// Children maybe
|
||||
else {
|
||||
// no common path
|
||||
if (ex < xmin || ex >= xmax) {
|
||||
e.classList.add("hide");
|
||||
}
|
||||
else {
|
||||
zoom_child(e, xmin, width);
|
||||
update_text(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function unzoom() {
|
||||
unzoombtn.classList.add("hide");
|
||||
var el = frames.children;
|
||||
for(var i = 0; i < el.length; i++) {
|
||||
el[i].classList.remove("parent");
|
||||
el[i].classList.remove("hide");
|
||||
zoom_reset(el[i]);
|
||||
update_text(el[i]);
|
||||
}
|
||||
}
|
||||
// search
|
||||
function reset_search() {
|
||||
var el = document.querySelectorAll("#frames rect");
|
||||
for (var i = 0; i < el.length; i++) {
|
||||
orig_load(el[i], "fill")
|
||||
}
|
||||
var params = get_params();
|
||||
delete params.s;
|
||||
history.replaceState(null, null, parse_params(params));
|
||||
}
|
||||
function search_prompt() {
|
||||
if (!searching) {
|
||||
var term = prompt("Enter a search term (regexp " +
|
||||
"allowed, eg: ^ext4_)", "");
|
||||
if (term != null) {
|
||||
search(term)
|
||||
}
|
||||
} else {
|
||||
reset_search();
|
||||
searching = 0;
|
||||
searchbtn.classList.remove("show");
|
||||
searchbtn.firstChild.nodeValue = "Search"
|
||||
matchedtxt.classList.add("hide");
|
||||
matchedtxt.firstChild.nodeValue = ""
|
||||
}
|
||||
}
|
||||
function search(term) {
|
||||
var re = new RegExp(term);
|
||||
var el = frames.children;
|
||||
var matches = new Object();
|
||||
var maxwidth = 0;
|
||||
for (var i = 0; i < el.length; i++) {
|
||||
var e = el[i];
|
||||
// Skip over frames which are either not visible, or below the zoomed-to frame
|
||||
if (e.classList.contains("hide") || e.classList.contains("parent")) {
|
||||
continue;
|
||||
}
|
||||
var func = g_to_func(e);
|
||||
var rect = find_child(e, "rect");
|
||||
if (func == null || rect == null)
|
||||
continue;
|
||||
// Save max width. Only works as we have a root frame
|
||||
var w = parseInt(rect.attributes["fg:w"].value);
|
||||
if (w > maxwidth)
|
||||
maxwidth = w;
|
||||
if (func.match(re)) {
|
||||
// highlight
|
||||
var x = parseInt(rect.attributes["fg:x"].value);
|
||||
orig_save(rect, "fill");
|
||||
rect.attributes.fill.value = searchcolor;
|
||||
// remember matches
|
||||
if (matches[x] == undefined) {
|
||||
matches[x] = w;
|
||||
} else {
|
||||
if (w > matches[x]) {
|
||||
// overwrite with parent
|
||||
matches[x] = w;
|
||||
}
|
||||
}
|
||||
searching = 1;
|
||||
}
|
||||
}
|
||||
if (!searching)
|
||||
return;
|
||||
var params = get_params();
|
||||
params.s = term;
|
||||
history.replaceState(null, null, parse_params(params));
|
||||
|
||||
searchbtn.classList.add("show");
|
||||
searchbtn.firstChild.nodeValue = "Reset Search";
|
||||
// calculate percent matched, excluding vertical overlap
|
||||
var count = 0;
|
||||
var lastx = -1;
|
||||
var lastw = 0;
|
||||
var keys = Array();
|
||||
for (k in matches) {
|
||||
if (matches.hasOwnProperty(k))
|
||||
keys.push(k);
|
||||
}
|
||||
// sort the matched frames by their x location
|
||||
// ascending, then width descending
|
||||
keys.sort(function(a, b){
|
||||
return a - b;
|
||||
});
|
||||
// Step through frames saving only the biggest bottom-up frames
|
||||
// thanks to the sort order. This relies on the tree property
|
||||
// where children are always smaller than their parents.
|
||||
for (var k in keys) {
|
||||
var x = parseInt(keys[k]);
|
||||
var w = matches[keys[k]];
|
||||
if (x >= lastx + lastw) {
|
||||
count += w;
|
||||
lastx = x;
|
||||
lastw = w;
|
||||
}
|
||||
}
|
||||
// display matched percent
|
||||
matchedtxt.classList.remove("hide");
|
||||
var pct = 100 * count / maxwidth;
|
||||
if (pct != 100) pct = pct.toFixed(1);
|
||||
matchedtxt.firstChild.nodeValue = "Matched: " + pct + "%";
|
||||
}
|
||||
function format_percent(n) {
|
||||
return n.toFixed(4) + "%";
|
||||
}
|
||||
]]></script><rect x="0" y="0" width="100%" height="518" fill="url(#background)"/><text id="title" x="50.0000%" y="24.00">Flame Graph</text><text id="details" x="10" y="501.00"> </text><text id="unzoom" class="hide" x="10" y="24.00">Reset Zoom</text><text id="search" x="1090" y="24.00">Search</text><text id="matched" x="1090" y="501.00"> </text><svg id="frames" x="10" width="1180" total_samples="55"><g><title>jsframework-a8f4acf5955e8e7f`core::ptr::drop_in_place<dioxus_core::virtual_dom::VirtualDom> (1 samples, 1.82%)</title><rect x="0.0000%" y="261" width="1.8182%" height="15" fill="rgb(227,0,7)" fg:x="0" fg:w="1"/><text x="0.2500%" y="271.50">j..</text></g><g><title>jsframework-a8f4acf5955e8e7f`core::ptr::drop_in_place<alloc::boxed::Box<dioxus_core::scopearena::ScopeArena>> (1 samples, 1.82%)</title><rect x="0.0000%" y="245" width="1.8182%" height="15" fill="rgb(217,0,24)" fg:x="0" fg:w="1"/><text x="0.2500%" y="255.50">j..</text></g><g><title>jsframework-a8f4acf5955e8e7f`<bumpalo::Bump as core::ops::drop::Drop>::drop (1 samples, 1.82%)</title><rect x="0.0000%" y="229" width="1.8182%" height="15" fill="rgb(221,193,54)" fg:x="0" fg:w="1"/><text x="0.2500%" y="239.50">j..</text></g><g><title>libsystem_malloc.dylib`free_medium (1 samples, 1.82%)</title><rect x="0.0000%" y="213" width="1.8182%" height="15" fill="rgb(248,212,6)" fg:x="0" fg:w="1"/><text x="0.2500%" y="223.50">l..</text></g><g><title>libsystem_kernel.dylib`madvise (1 samples, 1.82%)</title><rect x="0.0000%" y="197" width="1.8182%" height="15" fill="rgb(208,68,35)" fg:x="0" fg:w="1"/><text x="0.2500%" y="207.50">l..</text></g><g><title>libsystem_malloc.dylib`_malloc_zone_malloc (2 samples, 3.64%)</title><rect x="9.0909%" y="181" width="3.6364%" height="15" fill="rgb(232,128,0)" fg:x="5" fg:w="2"/><text x="9.3409%" y="191.50">libs..</text></g><g><title>libsystem_malloc.dylib`szone_malloc_should_clear (2 samples, 3.64%)</title><rect x="9.0909%" y="165" width="3.6364%" height="15" fill="rgb(207,160,47)" fg:x="5" fg:w="2"/><text x="9.3409%" y="175.50">libs..</text></g><g><title>libsystem_malloc.dylib`tiny_malloc_should_clear (2 samples, 3.64%)</title><rect x="9.0909%" y="149" width="3.6364%" height="15" fill="rgb(228,23,34)" fg:x="5" fg:w="2"/><text x="9.3409%" y="159.50">libs..</text></g><g><title>libsystem_malloc.dylib`tiny_malloc_from_free_list (1 samples, 1.82%)</title><rect x="10.9091%" y="133" width="1.8182%" height="15" fill="rgb(218,30,26)" fg:x="6" fg:w="1"/><text x="11.1591%" y="143.50">l..</text></g><g><title>libsystem_malloc.dylib`tiny_free_list_add_ptr (1 samples, 1.82%)</title><rect x="10.9091%" y="117" width="1.8182%" height="15" fill="rgb(220,122,19)" fg:x="6" fg:w="1"/><text x="11.1591%" y="127.50">l..</text></g><g><title>jsframework-a8f4acf5955e8e7f`dioxus_core::diff::DiffState::create_element_node (7 samples, 12.73%)</title><rect x="1.8182%" y="229" width="12.7273%" height="15" fill="rgb(250,228,42)" fg:x="1" fg:w="7"/><text x="2.0682%" y="239.50">jsframework-a8f4acf..</text></g><g><title>jsframework-a8f4acf5955e8e7f`alloc::raw_vec::RawVec<T,A>::reserve::do_reserve_and_handle (3 samples, 5.45%)</title><rect x="9.0909%" y="213" width="5.4545%" height="15" fill="rgb(240,193,28)" fg:x="5" fg:w="3"/><text x="9.3409%" y="223.50">jsframe..</text></g><g><title>jsframework-a8f4acf5955e8e7f`alloc::raw_vec::finish_grow (3 samples, 5.45%)</title><rect x="9.0909%" y="197" width="5.4545%" height="15" fill="rgb(216,20,37)" fg:x="5" fg:w="3"/><text x="9.3409%" y="207.50">jsframe..</text></g><g><title>libsystem_malloc.dylib`realloc (1 samples, 1.82%)</title><rect x="12.7273%" y="181" width="1.8182%" height="15" fill="rgb(206,188,39)" fg:x="7" fg:w="1"/><text x="12.9773%" y="191.50">l..</text></g><g><title>libsystem_malloc.dylib`malloc_zone_realloc (1 samples, 1.82%)</title><rect x="12.7273%" y="165" width="1.8182%" height="15" fill="rgb(217,207,13)" fg:x="7" fg:w="1"/><text x="12.9773%" y="175.50">l..</text></g><g><title>libsystem_malloc.dylib`szone_realloc (1 samples, 1.82%)</title><rect x="12.7273%" y="149" width="1.8182%" height="15" fill="rgb(231,73,38)" fg:x="7" fg:w="1"/><text x="12.9773%" y="159.50">l..</text></g><g><title>libsystem_platform.dylib`_platform_memmove$VARIANT$Haswell (1 samples, 1.82%)</title><rect x="12.7273%" y="133" width="1.8182%" height="15" fill="rgb(225,20,46)" fg:x="7" fg:w="1"/><text x="12.9773%" y="143.50">l..</text></g><g><title>jsframework-a8f4acf5955e8e7f`dioxus_core::scopearena::ScopeArena::fin_head (1 samples, 1.82%)</title><rect x="16.3636%" y="213" width="1.8182%" height="15" fill="rgb(210,31,41)" fg:x="9" fg:w="1"/><text x="16.6136%" y="223.50">j..</text></g><g><title>jsframework-a8f4acf5955e8e7f`<bumpalo::Bump as core::default::Default>::default (4 samples, 7.27%)</title><rect x="21.8182%" y="197" width="7.2727%" height="15" fill="rgb(221,200,47)" fg:x="12" fg:w="4"/><text x="22.0682%" y="207.50">jsframewor..</text></g><g><title>jsframework-a8f4acf5955e8e7f`<bumpalo::BLine truncated
|
||||
|
After Width: | Height: | Size: 38 KiB |
@@ -5,12 +5,12 @@
|
||||
//! if the type supports PartialEq. The Properties trait is used by the rsx! and html! macros to generate the type-safe builder
|
||||
//! that ensures compile-time required and optional fields on cx.
|
||||
|
||||
use crate::innerlude::{Context, Element, LazyNodes, NodeLink};
|
||||
use crate::innerlude::{Context, Element, LazyNodes, VPortal};
|
||||
|
||||
pub struct FragmentProps(Element);
|
||||
pub struct FragmentBuilder<const BUILT: bool>(Element);
|
||||
impl FragmentBuilder<false> {
|
||||
pub fn children(self, children: Option<NodeLink>) -> FragmentBuilder<true> {
|
||||
pub fn children(self, children: Option<VPortal>) -> FragmentBuilder<true> {
|
||||
FragmentBuilder(children)
|
||||
}
|
||||
}
|
||||
|
||||
+108
-88
@@ -109,7 +109,6 @@ pub struct DiffState<'bump> {
|
||||
scopes: &'bump ScopeArena,
|
||||
pub mutations: Mutations<'bump>,
|
||||
pub(crate) stack: DiffStack<'bump>,
|
||||
pub seen_scopes: FxHashSet<ScopeId>,
|
||||
pub force_diff: bool,
|
||||
}
|
||||
|
||||
@@ -119,7 +118,6 @@ impl<'bump> DiffState<'bump> {
|
||||
scopes,
|
||||
mutations: Mutations::new(),
|
||||
stack: DiffStack::new(),
|
||||
seen_scopes: Default::default(),
|
||||
force_diff: false,
|
||||
}
|
||||
}
|
||||
@@ -198,12 +196,13 @@ impl<'bump> DiffStack<'bump> {
|
||||
}
|
||||
}
|
||||
|
||||
fn push_subtree(&mut self) {
|
||||
self.nodes_created_stack.push(0);
|
||||
self.instructions.push(DiffInstruction::Mount {
|
||||
and: MountType::Append,
|
||||
});
|
||||
}
|
||||
// todo: subtrees
|
||||
// fn push_subtree(&mut self) {
|
||||
// self.nodes_created_stack.push(0);
|
||||
// self.instructions.push(DiffInstruction::Mount {
|
||||
// and: MountType::Append,
|
||||
// });
|
||||
// }
|
||||
|
||||
fn push_nodes_created(&mut self, count: usize) {
|
||||
self.nodes_created_stack.push(count);
|
||||
@@ -284,12 +283,12 @@ impl<'bump> DiffState<'bump> {
|
||||
// recursively push all the nodes of a tree onto the stack and return how many are there
|
||||
fn push_all_nodes(&mut self, node: &'bump VNode<'bump>) -> usize {
|
||||
match node {
|
||||
VNode::Text(_) | VNode::Anchor(_) | VNode::Suspended(_) => {
|
||||
VNode::Text(_) | VNode::Placeholder(_) => {
|
||||
self.mutations.push_root(node.mounted_id());
|
||||
1
|
||||
}
|
||||
|
||||
VNode::Linked(linked) => {
|
||||
VNode::Portal(linked) => {
|
||||
let node = unsafe { &*linked.node };
|
||||
let node: &VNode = unsafe { std::mem::transmute(node) };
|
||||
self.push_all_nodes(node)
|
||||
@@ -354,12 +353,11 @@ impl<'bump> DiffState<'bump> {
|
||||
fn create_node(&mut self, node: &'bump VNode<'bump>) {
|
||||
match node {
|
||||
VNode::Text(vtext) => self.create_text_node(vtext, node),
|
||||
VNode::Suspended(suspended) => self.create_suspended_node(suspended, node),
|
||||
VNode::Anchor(anchor) => self.create_anchor_node(anchor, node),
|
||||
VNode::Placeholder(anchor) => self.create_anchor_node(anchor, node),
|
||||
VNode::Element(element) => self.create_element_node(element, node),
|
||||
VNode::Fragment(frag) => self.create_fragment_node(frag),
|
||||
VNode::Component(component) => self.create_component_node(component),
|
||||
VNode::Linked(linked) => self.create_linked_node(linked),
|
||||
VNode::Component(component) => self.create_component_node(*component),
|
||||
VNode::Portal(linked) => self.create_linked_node(linked),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,17 +369,7 @@ impl<'bump> DiffState<'bump> {
|
||||
self.stack.add_child_count(1);
|
||||
}
|
||||
|
||||
fn create_suspended_node(&mut self, suspended: &'bump VSuspended, node: &'bump VNode<'bump>) {
|
||||
let real_id = self.scopes.reserve_node(node);
|
||||
self.mutations.create_placeholder(real_id);
|
||||
|
||||
suspended.dom_id.set(Some(real_id));
|
||||
self.stack.add_child_count(1);
|
||||
|
||||
self.attach_suspended_node_to_scope(suspended);
|
||||
}
|
||||
|
||||
fn create_anchor_node(&mut self, anchor: &'bump VAnchor, node: &'bump VNode<'bump>) {
|
||||
fn create_anchor_node(&mut self, anchor: &'bump VPlaceholder, node: &'bump VNode<'bump>) {
|
||||
let real_id = self.scopes.reserve_node(node);
|
||||
|
||||
self.mutations.create_placeholder(real_id);
|
||||
@@ -433,6 +421,15 @@ impl<'bump> DiffState<'bump> {
|
||||
self.mutations.set_attribute(attr, real_id.as_u64());
|
||||
}
|
||||
|
||||
// todo: the settext optimization
|
||||
|
||||
// if children.len() == 1 {
|
||||
// if let VNode::Text(vtext) = children[0] {
|
||||
// self.mutations.set_text(vtext.text, real_id.as_u64());
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
|
||||
if !children.is_empty() {
|
||||
self.stack.create_children(children, MountType::Append);
|
||||
}
|
||||
@@ -460,8 +457,6 @@ impl<'bump> DiffState<'bump> {
|
||||
self.scopes
|
||||
.new_with_key(fc_ptr, caller, parent_scope, container, height, subtree);
|
||||
|
||||
log::debug!("container for scope {:?} is {:?}", new_idx, container);
|
||||
|
||||
// Actually initialize the caller's slot with the right address
|
||||
vcomponent.associated_scope.set(Some(new_idx));
|
||||
|
||||
@@ -469,12 +464,12 @@ impl<'bump> DiffState<'bump> {
|
||||
let cur_scope = self.scopes.get_scope(&parent_idx).unwrap();
|
||||
let extended = unsafe { std::mem::transmute(vcomponent) };
|
||||
cur_scope.items.borrow_mut().borrowed_props.push(extended);
|
||||
} else {
|
||||
// the props are currently bump allocated but we need to move them to the heap
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// add noderefs to current noderef list Noderefs
|
||||
// add effects to current effect list Effects
|
||||
let new_component = self.scopes.get_scope(&new_idx).unwrap();
|
||||
// TODO: add noderefs to current noderef list Noderefs
|
||||
let _new_component = self.scopes.get_scope(&new_idx).unwrap();
|
||||
|
||||
log::debug!(
|
||||
"initializing component {:?} with height {:?}",
|
||||
@@ -488,16 +483,17 @@ impl<'bump> DiffState<'bump> {
|
||||
let nextnode = self.scopes.fin_head(&new_idx);
|
||||
self.stack.create_component(new_idx, nextnode);
|
||||
|
||||
if new_component.is_subtree_root.get() {
|
||||
self.stack.push_subtree();
|
||||
}
|
||||
// todo: subtrees
|
||||
// if new_component.is_subtree_root.get() {
|
||||
// self.stack.push_subtree();
|
||||
// }
|
||||
}
|
||||
|
||||
// Finally, insert this scope as a seen node.
|
||||
self.seen_scopes.insert(new_idx);
|
||||
self.mutations.dirty_scopes.insert(new_idx);
|
||||
}
|
||||
|
||||
fn create_linked_node(&mut self, link: &'bump NodeLink) {
|
||||
fn create_linked_node(&mut self, link: &'bump VPortal) {
|
||||
if link.scope_id.get().is_none() {
|
||||
if let Some(cur_scope) = self.stack.current_scope() {
|
||||
link.scope_id.set(Some(cur_scope));
|
||||
@@ -519,20 +515,17 @@ impl<'bump> DiffState<'bump> {
|
||||
self.diff_text_nodes(old, new, old_node, new_node);
|
||||
}
|
||||
(Component(old), Component(new)) => {
|
||||
self.diff_component_nodes(old_node, new_node, old, new)
|
||||
self.diff_component_nodes(old_node, new_node, *old, *new)
|
||||
}
|
||||
(Fragment(old), Fragment(new)) => self.diff_fragment_nodes(old, new),
|
||||
(Anchor(old), Anchor(new)) => new.dom_id.set(old.dom_id.get()),
|
||||
(Suspended(old), Suspended(new)) => self.diff_suspended_nodes(old, new),
|
||||
(Placeholder(old), Placeholder(new)) => new.dom_id.set(old.dom_id.get()),
|
||||
(Element(old), Element(new)) => self.diff_element_nodes(old, new, old_node, new_node),
|
||||
(Linked(old), Linked(new)) => self.diff_linked_nodes(old, new),
|
||||
(Portal(old), Portal(new)) => self.diff_linked_nodes(old, new),
|
||||
|
||||
// Anything else is just a basic replace and create
|
||||
(
|
||||
Linked(_) | Component(_) | Fragment(_) | Text(_) | Element(_) | Anchor(_)
|
||||
| Suspended(_),
|
||||
Linked(_) | Component(_) | Fragment(_) | Text(_) | Element(_) | Anchor(_)
|
||||
| Suspended(_),
|
||||
Portal(_) | Component(_) | Fragment(_) | Text(_) | Element(_) | Placeholder(_),
|
||||
Portal(_) | Component(_) | Fragment(_) | Text(_) | Element(_) | Placeholder(_),
|
||||
) => self
|
||||
.stack
|
||||
.create_node(new_node, MountType::Replace { old: old_node }),
|
||||
@@ -657,6 +650,64 @@ impl<'bump> DiffState<'bump> {
|
||||
self.stack.instructions.push(DiffInstruction::PopElement);
|
||||
self.diff_children(old.children, new.children);
|
||||
}
|
||||
|
||||
// todo: this is for the "settext" optimization
|
||||
// it works, but i'm not sure if it's the direction we want to take
|
||||
|
||||
// match (old.children.len(), new.children.len()) {
|
||||
// (0, 0) => {}
|
||||
// (1, 1) => {
|
||||
// let old1 = &old.children[0];
|
||||
// let new1 = &new.children[0];
|
||||
|
||||
// match (old1, new1) {
|
||||
// (VNode::Text(old_text), VNode::Text(new_text)) => {
|
||||
// if old_text.text != new_text.text {
|
||||
// self.mutations.set_text(new_text.text, root.as_u64());
|
||||
// }
|
||||
// }
|
||||
// (VNode::Text(_old_text), _) => {
|
||||
// self.stack.element_stack.push(root);
|
||||
// self.stack.instructions.push(DiffInstruction::PopElement);
|
||||
// self.stack.create_node(new1, MountType::Append);
|
||||
// }
|
||||
// (_, VNode::Text(new_text)) => {
|
||||
// self.remove_nodes([old1], false);
|
||||
// self.mutations.set_text(new_text.text, root.as_u64());
|
||||
// }
|
||||
// _ => {
|
||||
// self.stack.element_stack.push(root);
|
||||
// self.stack.instructions.push(DiffInstruction::PopElement);
|
||||
// self.diff_children(old.children, new.children);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// (0, 1) => {
|
||||
// if let VNode::Text(text) = &new.children[0] {
|
||||
// self.mutations.set_text(text.text, root.as_u64());
|
||||
// } else {
|
||||
// self.stack.element_stack.push(root);
|
||||
// self.stack.instructions.push(DiffInstruction::PopElement);
|
||||
// }
|
||||
// }
|
||||
// (0, _) => {
|
||||
// self.mutations.edits.push(PushRoot {
|
||||
// root: root.as_u64(),
|
||||
// });
|
||||
// self.stack.element_stack.push(root);
|
||||
// self.stack.instructions.push(DiffInstruction::PopElement);
|
||||
// self.stack.create_children(new.children, MountType::Append);
|
||||
// }
|
||||
// (_, 0) => {
|
||||
// self.remove_nodes(old.children, false);
|
||||
// self.mutations.set_text("", root.as_u64());
|
||||
// }
|
||||
// (_, _) => {
|
||||
// self.stack.element_stack.push(root);
|
||||
// self.stack.instructions.push(DiffInstruction::PopElement);
|
||||
// self.diff_children(old.children, new.children);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
fn diff_component_nodes(
|
||||
@@ -688,6 +739,7 @@ impl<'bump> DiffState<'bump> {
|
||||
.get_scope_mut(&scope_addr)
|
||||
.unwrap_or_else(|| panic!("could not find {:?}", scope_addr))
|
||||
};
|
||||
|
||||
scope.caller = unsafe { std::mem::transmute(new.caller) };
|
||||
|
||||
// React doesn't automatically memoize, but we do.
|
||||
@@ -721,12 +773,7 @@ impl<'bump> DiffState<'bump> {
|
||||
self.diff_children(old.children, new.children);
|
||||
}
|
||||
|
||||
fn diff_suspended_nodes(&mut self, old: &'bump VSuspended, new: &'bump VSuspended) {
|
||||
new.dom_id.set(old.dom_id.get());
|
||||
self.attach_suspended_node_to_scope(new);
|
||||
}
|
||||
|
||||
fn diff_linked_nodes(&mut self, old: &'bump NodeLink, new: &'bump NodeLink) {
|
||||
fn diff_linked_nodes(&mut self, old: &'bump VPortal, new: &'bump VPortal) {
|
||||
if !std::ptr::eq(old.node, new.node) {
|
||||
// if the ptrs are the same then theyr're the same
|
||||
let old: &VNode = unsafe { std::mem::transmute(&*old.node) };
|
||||
@@ -769,14 +816,14 @@ impl<'bump> DiffState<'bump> {
|
||||
(_, []) => {
|
||||
self.remove_nodes(old, true);
|
||||
}
|
||||
([VNode::Anchor(old_anchor)], [VNode::Anchor(new_anchor)]) => {
|
||||
([VNode::Placeholder(old_anchor)], [VNode::Placeholder(new_anchor)]) => {
|
||||
old_anchor.dom_id.set(new_anchor.dom_id.get());
|
||||
}
|
||||
([VNode::Anchor(_)], _) => {
|
||||
([VNode::Placeholder(_)], _) => {
|
||||
self.stack
|
||||
.create_children(new, MountType::Replace { old: &old[0] });
|
||||
}
|
||||
(_, [VNode::Anchor(_)]) => {
|
||||
(_, [VNode::Placeholder(_)]) => {
|
||||
let new: &'bump VNode<'bump> = &new[0];
|
||||
if let Some(first_old) = old.get(0) {
|
||||
self.remove_nodes(&old[1..], true);
|
||||
@@ -1178,9 +1225,8 @@ impl<'bump> DiffState<'bump> {
|
||||
match &search_node.take().unwrap() {
|
||||
VNode::Text(t) => break t.dom_id.get(),
|
||||
VNode::Element(t) => break t.dom_id.get(),
|
||||
VNode::Suspended(t) => break t.dom_id.get(),
|
||||
VNode::Anchor(t) => break t.dom_id.get(),
|
||||
VNode::Linked(l) => {
|
||||
VNode::Placeholder(t) => break t.dom_id.get(),
|
||||
VNode::Portal(l) => {
|
||||
let node: &VNode = unsafe { std::mem::transmute(&*l.node) };
|
||||
self.find_last_element(node);
|
||||
}
|
||||
@@ -1208,14 +1254,13 @@ impl<'bump> DiffState<'bump> {
|
||||
let scope_id = el.associated_scope.get().unwrap();
|
||||
search_node = Some(self.scopes.root_node(&scope_id));
|
||||
}
|
||||
VNode::Linked(link) => {
|
||||
VNode::Portal(link) => {
|
||||
let node = unsafe { std::mem::transmute(&*link.node) };
|
||||
search_node = Some(node);
|
||||
}
|
||||
VNode::Text(t) => break t.dom_id.get(),
|
||||
VNode::Element(t) => break t.dom_id.get(),
|
||||
VNode::Suspended(t) => break t.dom_id.get(),
|
||||
VNode::Anchor(t) => break t.dom_id.get(),
|
||||
VNode::Placeholder(t) => break t.dom_id.get(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1233,7 +1278,7 @@ impl<'bump> DiffState<'bump> {
|
||||
self.remove_nodes(el.children, false);
|
||||
}
|
||||
|
||||
VNode::Text(_) | VNode::Anchor(_) | VNode::Suspended(_) => {
|
||||
VNode::Text(_) | VNode::Placeholder(_) => {
|
||||
let id = old
|
||||
.try_mounted_id()
|
||||
.unwrap_or_else(|| panic!("broke on {:?}", old));
|
||||
@@ -1255,7 +1300,7 @@ impl<'bump> DiffState<'bump> {
|
||||
self.scopes.try_remove(&scope_id).unwrap();
|
||||
}
|
||||
|
||||
VNode::Linked(l) => {
|
||||
VNode::Portal(l) => {
|
||||
let node: &'bump VNode<'bump> = unsafe { std::mem::transmute(&*l.node) };
|
||||
self.replace_node(node, nodes_created);
|
||||
}
|
||||
@@ -1282,15 +1327,7 @@ impl<'bump> DiffState<'bump> {
|
||||
}
|
||||
}
|
||||
}
|
||||
VNode::Suspended(s) => {
|
||||
let id = s.dom_id.get().unwrap();
|
||||
self.scopes.collect_garbage(id);
|
||||
|
||||
if gen_muts {
|
||||
self.mutations.remove(id.as_u64());
|
||||
}
|
||||
}
|
||||
VNode::Anchor(a) => {
|
||||
VNode::Placeholder(a) => {
|
||||
let id = a.dom_id.get().unwrap();
|
||||
self.scopes.collect_garbage(id);
|
||||
|
||||
@@ -1312,7 +1349,7 @@ impl<'bump> DiffState<'bump> {
|
||||
self.remove_nodes(f.children, gen_muts);
|
||||
}
|
||||
|
||||
VNode::Linked(l) => {
|
||||
VNode::Portal(l) => {
|
||||
let node = unsafe { std::mem::transmute(&*l.node) };
|
||||
self.remove_nodes(Some(node), gen_muts);
|
||||
}
|
||||
@@ -1337,21 +1374,4 @@ impl<'bump> DiffState<'bump> {
|
||||
let long_listener = unsafe { std::mem::transmute(listener) };
|
||||
scope.items.borrow_mut().listeners.push(long_listener)
|
||||
}
|
||||
|
||||
fn attach_suspended_node_to_scope(&mut self, suspended: &'bump VSuspended) {
|
||||
if let Some(scope) = self
|
||||
.stack
|
||||
.current_scope()
|
||||
.and_then(|id| self.scopes.get_scope(&id))
|
||||
{
|
||||
todo!()
|
||||
// // safety: this lifetime is managed by the logic on scope
|
||||
// let extended = unsafe { std::mem::transmute(suspended) };
|
||||
// scope
|
||||
// .items
|
||||
// .borrow_mut()
|
||||
// .suspended_nodes
|
||||
// .insert(suspended.task_id, extended);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,14 @@ enum StackNodeStorage<'a, 'b> {
|
||||
}
|
||||
|
||||
impl<'a, 'b> LazyNodes<'a, 'b> {
|
||||
|
||||
pub fn new_some<F>(_val: F) -> Option<Self>
|
||||
where
|
||||
F: FnOnce(NodeFactory<'a>) -> VNode<'a> + 'b,
|
||||
{
|
||||
Some(Self::new(_val))
|
||||
}
|
||||
|
||||
pub fn new<F>(_val: F) -> Self
|
||||
where
|
||||
F: FnOnce(NodeFactory<'a>) -> VNode<'a> + 'b,
|
||||
@@ -221,7 +229,7 @@ fn it_works() {
|
||||
|
||||
let factory = NodeFactory { bump: &bump };
|
||||
|
||||
let caller = NodeFactory::annotate_lazy(|f| {
|
||||
let caller = LazyNodes::new_some(|f| {
|
||||
//
|
||||
f.text(format_args!("hello world!"))
|
||||
})
|
||||
@@ -254,7 +262,7 @@ fn it_drops() {
|
||||
.map(|i| {
|
||||
let val = val.clone();
|
||||
|
||||
NodeFactory::annotate_lazy(move |f| {
|
||||
LazyNodes::new_some(move |f| {
|
||||
log::debug!("hell closure");
|
||||
let inner = DropInner { id: i };
|
||||
f.text(format_args!("hello world {:?}, {:?}", inner.id, val))
|
||||
@@ -262,7 +270,7 @@ fn it_drops() {
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
NodeFactory::annotate_lazy(|f| {
|
||||
LazyNodes::new_some(|f| {
|
||||
log::debug!("main closure");
|
||||
f.fragment_from_iter(it)
|
||||
})
|
||||
|
||||
@@ -31,14 +31,14 @@ pub(crate) mod innerlude {
|
||||
pub use crate::scopearena::*;
|
||||
pub use crate::virtual_dom::*;
|
||||
|
||||
pub type Element = Option<NodeLink>;
|
||||
pub type Element = Option<VPortal>;
|
||||
pub type FC<P> = for<'a> fn(Context<'a>, &'a P) -> Element;
|
||||
}
|
||||
|
||||
pub use crate::innerlude::{
|
||||
Attribute, Context, DioxusElement, DomEdit, Element, ElementId, EventPriority, IntoVNode,
|
||||
LazyNodes, Listener, MountType, Mutations, NodeFactory, Properties, SchedulerMsg, ScopeId,
|
||||
UserEvent, VAnchor, VElement, VFragment, VNode, VSuspended, VirtualDom, FC,
|
||||
UserEvent, VElement, VFragment, VNode, VirtualDom, FC,
|
||||
};
|
||||
|
||||
pub mod prelude {
|
||||
|
||||
@@ -14,6 +14,7 @@ use std::{any::Any, fmt::Debug};
|
||||
/// Mutations are the only link between the RealDOM and the VirtualDOM.
|
||||
pub struct Mutations<'a> {
|
||||
pub edits: Vec<DomEdit<'a>>,
|
||||
pub dirty_scopes: FxHashSet<ScopeId>,
|
||||
pub refs: Vec<NodeRefMutation<'a>>,
|
||||
pub effects: Vec<&'a dyn FnMut()>,
|
||||
}
|
||||
@@ -103,6 +104,7 @@ pub enum DomEdit<'bump> {
|
||||
},
|
||||
}
|
||||
|
||||
use fxhash::FxHashSet;
|
||||
use DomEdit::*;
|
||||
|
||||
impl<'a> Mutations<'a> {
|
||||
@@ -110,6 +112,7 @@ impl<'a> Mutations<'a> {
|
||||
Self {
|
||||
edits: Vec::new(),
|
||||
refs: Vec::new(),
|
||||
dirty_scopes: Default::default(),
|
||||
effects: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
+58
-91
@@ -97,19 +97,9 @@ pub enum VNode<'src> {
|
||||
/// ```
|
||||
Component(&'src VComponent<'src>),
|
||||
|
||||
/// Suspended VNodes represent chunks of the UI tree that are not yet ready to be displayed.
|
||||
/// Placeholders are a type of placeholder VNode used when fragments don't contain any children.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust, ignore
|
||||
///
|
||||
///
|
||||
/// ```
|
||||
Suspended(&'src VSuspended),
|
||||
|
||||
/// Anchors are a type of placeholder VNode used when fragments don't contain any children.
|
||||
///
|
||||
/// Anchors cannot be directly constructed via public APIs.
|
||||
/// Placeholders cannot be directly constructed via public APIs.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
@@ -123,7 +113,7 @@ pub enum VNode<'src> {
|
||||
/// assert_eq!(root, VNode::Anchor);
|
||||
/// }
|
||||
/// ```
|
||||
Anchor(&'src VAnchor),
|
||||
Placeholder(&'src VPlaceholder),
|
||||
|
||||
/// A VNode that is actually a pointer to some nodes rather than the nodes directly. Useful when rendering portals
|
||||
/// or eliding lifetimes on VNodes through runtime checks.
|
||||
@@ -139,7 +129,7 @@ pub enum VNode<'src> {
|
||||
///
|
||||
/// let node: NodeLink = vdom.render_vnode(rsx!( "hello" ));
|
||||
/// ```
|
||||
Linked(NodeLink),
|
||||
Portal(VPortal),
|
||||
}
|
||||
|
||||
impl<'src> VNode<'src> {
|
||||
@@ -149,11 +139,9 @@ impl<'src> VNode<'src> {
|
||||
VNode::Element(el) => el.key,
|
||||
VNode::Component(c) => c.key,
|
||||
VNode::Fragment(f) => f.key,
|
||||
|
||||
VNode::Text(_t) => None,
|
||||
VNode::Suspended(_s) => None,
|
||||
VNode::Anchor(_f) => None,
|
||||
VNode::Linked(_c) => None,
|
||||
VNode::Placeholder(_f) => None,
|
||||
VNode::Portal(_c) => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,10 +159,8 @@ impl<'src> VNode<'src> {
|
||||
match &self {
|
||||
VNode::Text(el) => el.dom_id.get(),
|
||||
VNode::Element(el) => el.dom_id.get(),
|
||||
VNode::Anchor(el) => el.dom_id.get(),
|
||||
VNode::Suspended(el) => el.dom_id.get(),
|
||||
|
||||
VNode::Linked(_) => None,
|
||||
VNode::Placeholder(el) => el.dom_id.get(),
|
||||
VNode::Portal(_) => None,
|
||||
VNode::Fragment(_) => None,
|
||||
VNode::Component(_) => None,
|
||||
}
|
||||
@@ -183,7 +169,6 @@ impl<'src> VNode<'src> {
|
||||
pub(crate) fn children(&self) -> &[VNode<'src>] {
|
||||
match &self {
|
||||
VNode::Fragment(f) => f.children,
|
||||
VNode::Component(_c) => todo!("children are not accessible through this"),
|
||||
_ => &[],
|
||||
}
|
||||
}
|
||||
@@ -194,13 +179,12 @@ impl<'src> VNode<'src> {
|
||||
VNode::Text(t) => VNode::Text(*t),
|
||||
VNode::Element(e) => VNode::Element(*e),
|
||||
VNode::Component(c) => VNode::Component(*c),
|
||||
VNode::Suspended(s) => VNode::Suspended(*s),
|
||||
VNode::Anchor(a) => VNode::Anchor(*a),
|
||||
VNode::Placeholder(a) => VNode::Placeholder(*a),
|
||||
VNode::Fragment(f) => VNode::Fragment(VFragment {
|
||||
children: f.children,
|
||||
key: f.key,
|
||||
}),
|
||||
VNode::Linked(c) => VNode::Linked(NodeLink {
|
||||
VNode::Portal(c) => VNode::Portal(VPortal {
|
||||
scope_id: c.scope_id.clone(),
|
||||
link_idx: c.link_idx.clone(),
|
||||
node: c.node,
|
||||
@@ -217,16 +201,13 @@ impl Debug for VNode<'_> {
|
||||
.field("name", &el.tag_name)
|
||||
.field("key", &el.key)
|
||||
.finish(),
|
||||
|
||||
VNode::Text(t) => write!(s, "VNode::VText {{ text: {} }}", t.text),
|
||||
VNode::Anchor(_) => write!(s, "VNode::VAnchor"),
|
||||
|
||||
VNode::Placeholder(_) => write!(s, "VNode::VAnchor"),
|
||||
VNode::Fragment(frag) => {
|
||||
write!(s, "VNode::VFragment {{ children: {:?} }}", frag.children)
|
||||
}
|
||||
VNode::Suspended { .. } => write!(s, "VNode::VSuspended"),
|
||||
VNode::Component(comp) => write!(s, "VNode::VComponent {{ fc: {:?}}}", comp.user_fc),
|
||||
VNode::Linked(c) => write!(s, "VNode::VCached {{ scope_id: {:?} }}", c.scope_id.get()),
|
||||
VNode::Portal(c) => write!(s, "VNode::VCached {{ scope_id: {:?} }}", c.scope_id.get()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -254,7 +235,7 @@ fn empty_cell() -> Cell<Option<ElementId>> {
|
||||
}
|
||||
|
||||
/// A placeholder node only generated when Fragments don't have any children.
|
||||
pub struct VAnchor {
|
||||
pub struct VPlaceholder {
|
||||
pub dom_id: Cell<Option<ElementId>>,
|
||||
}
|
||||
|
||||
@@ -357,10 +338,37 @@ pub struct Listener<'bump> {
|
||||
pub event: &'static str,
|
||||
|
||||
/// The actual callback that the user specified
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub(crate) callback:
|
||||
RefCell<Option<BumpBox<'bump, dyn FnMut(std::sync::Arc<dyn Any + Send + Sync>) + 'bump>>>,
|
||||
}
|
||||
|
||||
/// A cached node is a "pointer" to a "rendered" node in a particular scope
|
||||
///
|
||||
/// It does not provide direct access to the node, so it doesn't carry any lifetime information with it
|
||||
///
|
||||
/// It is used during the diffing/rendering process as a runtime key into an existing set of nodes. The "render" key
|
||||
/// is essentially a unique key to guarantee safe usage of the Node.
|
||||
///
|
||||
/// Linked VNodes can only be made through the [`Context::render`] method
|
||||
///
|
||||
/// Typically, NodeLinks are found *not* in a VNode. When NodeLinks are in a VNode, the NodeLink was passed into
|
||||
/// an `rsx!` call.
|
||||
///
|
||||
/// todo: remove the raw pointer and use runtime checks instead
|
||||
#[derive(Debug)]
|
||||
pub struct VPortal {
|
||||
pub(crate) link_idx: Cell<usize>,
|
||||
pub(crate) scope_id: Cell<Option<ScopeId>>,
|
||||
pub(crate) node: *const VNode<'static>,
|
||||
}
|
||||
|
||||
impl PartialEq for VPortal {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.node == other.node
|
||||
}
|
||||
}
|
||||
|
||||
/// Virtual Components for custom user-defined components
|
||||
/// Only supports the functional syntax
|
||||
pub struct VComponent<'src> {
|
||||
@@ -379,45 +387,14 @@ pub struct VComponent<'src> {
|
||||
pub(crate) bump_props: *const (),
|
||||
|
||||
// during the "teardown" process we'll take the caller out so it can be dropped properly
|
||||
// pub(crate) caller: Option<VCompCaller<'src>>,
|
||||
pub(crate) caller: &'src dyn Fn(&'src Scope) -> Element,
|
||||
|
||||
pub(crate) comparator: Option<&'src dyn Fn(&VComponent) -> bool>,
|
||||
|
||||
// todo: re-add this
|
||||
pub(crate) drop_props: RefCell<Option<BumpBox<'src, dyn FnMut()>>>,
|
||||
}
|
||||
|
||||
pub struct VSuspended {
|
||||
pub task_id: usize,
|
||||
pub scope: Cell<Option<ScopeId>>,
|
||||
pub dom_id: Cell<Option<ElementId>>,
|
||||
pub parent: Cell<Option<ElementId>>,
|
||||
}
|
||||
|
||||
/// A cached node is a "pointer" to a "rendered" node in a particular scope
|
||||
///
|
||||
/// It does not provide direct access to the node, so it doesn't carry any lifetime information with it
|
||||
///
|
||||
/// It is used during the diffing/rendering process as a runtime key into an existing set of nodes. The "render" key
|
||||
/// is essentially a unique key to guarantee safe usage of the Node.
|
||||
///
|
||||
/// Linked VNodes can only be made through the [`Context::render`] method
|
||||
///
|
||||
/// Typically, NodeLinks are found *not* in a VNode. When NodeLinks are in a VNode, the NodeLink was passed into
|
||||
/// an `rsx!` call.
|
||||
#[derive(Debug)]
|
||||
pub struct NodeLink {
|
||||
pub(crate) link_idx: Cell<usize>,
|
||||
pub(crate) scope_id: Cell<Option<ScopeId>>,
|
||||
pub(crate) node: *const VNode<'static>,
|
||||
}
|
||||
|
||||
impl PartialEq for NodeLink {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.node == other.node
|
||||
}
|
||||
}
|
||||
|
||||
/// This struct provides an ergonomic API to quickly build VNodes.
|
||||
///
|
||||
/// NodeFactory is used to build VNodes in the component's memory space.
|
||||
@@ -637,8 +614,8 @@ impl<'a> NodeFactory<'a> {
|
||||
callback: BumpBox<'a, dyn FnMut(Arc<dyn Any + Send + Sync>) + 'a>,
|
||||
) -> Listener<'a> {
|
||||
Listener {
|
||||
mounted_node: Cell::new(None),
|
||||
event,
|
||||
mounted_node: Cell::new(None),
|
||||
callback: RefCell::new(Some(callback)),
|
||||
}
|
||||
}
|
||||
@@ -647,15 +624,14 @@ impl<'a> NodeFactory<'a> {
|
||||
self,
|
||||
node_iter: impl IntoIterator<Item = impl IntoVNode<'a> + 'c> + 'b,
|
||||
) -> VNode<'a> {
|
||||
let bump = self.bump;
|
||||
let mut nodes = bumpalo::collections::Vec::new_in(bump);
|
||||
let mut nodes = bumpalo::collections::Vec::new_in(self.bump);
|
||||
|
||||
for node in node_iter {
|
||||
nodes.push(node.into_vnode(self));
|
||||
}
|
||||
|
||||
if nodes.is_empty() {
|
||||
nodes.push(VNode::Anchor(bump.alloc(VAnchor {
|
||||
nodes.push(VNode::Placeholder(self.bump.alloc(VPlaceholder {
|
||||
dom_id: empty_cell(),
|
||||
})));
|
||||
}
|
||||
@@ -670,26 +646,23 @@ impl<'a> NodeFactory<'a> {
|
||||
self,
|
||||
node_iter: impl IntoIterator<Item = impl IntoVNode<'a> + 'c> + 'b,
|
||||
) -> VNode<'a> {
|
||||
let bump = self.bump;
|
||||
let mut nodes = bumpalo::collections::Vec::new_in(bump);
|
||||
let mut nodes = bumpalo::collections::Vec::new_in(self.bump);
|
||||
|
||||
for node in node_iter {
|
||||
nodes.push(node.into_vnode(self));
|
||||
}
|
||||
|
||||
if nodes.is_empty() {
|
||||
nodes.push(VNode::Anchor(bump.alloc(VAnchor {
|
||||
nodes.push(VNode::Placeholder(self.bump.alloc(VPlaceholder {
|
||||
dom_id: empty_cell(),
|
||||
})));
|
||||
}
|
||||
|
||||
let children = nodes.into_bump_slice();
|
||||
|
||||
// todo: add a backtrace
|
||||
if cfg!(debug_assertions) && children.len() > 1 && children.last().unwrap().key().is_none()
|
||||
{
|
||||
use backtrace::Backtrace;
|
||||
let bt = Backtrace::new();
|
||||
// todo: make the backtrace prettier or remove it altogether
|
||||
log::error!(
|
||||
r#"
|
||||
Warning: Each child in an array or iterator should have a unique "key" prop.
|
||||
@@ -698,7 +671,7 @@ impl<'a> NodeFactory<'a> {
|
||||
-------------
|
||||
{:?}
|
||||
"#,
|
||||
bt
|
||||
backtrace::Backtrace::new()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -722,7 +695,7 @@ impl<'a> NodeFactory<'a> {
|
||||
}
|
||||
|
||||
if nodes.is_empty() {
|
||||
nodes.push(VNode::Anchor(bump.alloc(VAnchor {
|
||||
nodes.push(VNode::Placeholder(bump.alloc(VPlaceholder {
|
||||
dom_id: empty_cell(),
|
||||
})));
|
||||
}
|
||||
@@ -753,18 +726,12 @@ impl<'a> NodeFactory<'a> {
|
||||
key: None,
|
||||
});
|
||||
let ptr = self.bump.alloc(frag) as *const _;
|
||||
Some(NodeLink {
|
||||
Some(VPortal {
|
||||
link_idx: Default::default(),
|
||||
scope_id: Default::default(),
|
||||
node: unsafe { std::mem::transmute(ptr) },
|
||||
})
|
||||
}
|
||||
|
||||
pub fn annotate_lazy<'z, 'b>(
|
||||
f: impl FnOnce(NodeFactory<'z>) -> VNode<'z> + 'b,
|
||||
) -> Option<LazyNodes<'z, 'b>> {
|
||||
Some(LazyNodes::new(f))
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for NodeFactory<'_> {
|
||||
@@ -860,10 +827,10 @@ impl IntoVNode<'_> for Arguments<'_> {
|
||||
}
|
||||
|
||||
// called cx.render from a helper function
|
||||
impl IntoVNode<'_> for Option<NodeLink> {
|
||||
impl IntoVNode<'_> for Option<VPortal> {
|
||||
fn into_vnode(self, _cx: NodeFactory) -> VNode {
|
||||
match self {
|
||||
Some(node) => VNode::Linked(node),
|
||||
Some(node) => VNode::Portal(node),
|
||||
None => {
|
||||
todo!()
|
||||
}
|
||||
@@ -873,10 +840,10 @@ impl IntoVNode<'_> for Option<NodeLink> {
|
||||
|
||||
// essentially passing elements through props
|
||||
// just build a new element in place
|
||||
impl IntoVNode<'_> for &Option<NodeLink> {
|
||||
impl IntoVNode<'_> for &Option<VPortal> {
|
||||
fn into_vnode(self, _cx: NodeFactory) -> VNode {
|
||||
match self {
|
||||
Some(node) => VNode::Linked(NodeLink {
|
||||
Some(node) => VNode::Portal(VPortal {
|
||||
link_idx: node.link_idx.clone(),
|
||||
scope_id: node.scope_id.clone(),
|
||||
node: node.node,
|
||||
@@ -889,15 +856,15 @@ impl IntoVNode<'_> for &Option<NodeLink> {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoVNode<'_> for NodeLink {
|
||||
impl IntoVNode<'_> for VPortal {
|
||||
fn into_vnode(self, _cx: NodeFactory) -> VNode {
|
||||
VNode::Linked(self)
|
||||
VNode::Portal(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoVNode<'_> for &NodeLink {
|
||||
impl IntoVNode<'_> for &VPortal {
|
||||
fn into_vnode(self, _cx: NodeFactory) -> VNode {
|
||||
VNode::Linked(NodeLink {
|
||||
VNode::Portal(VPortal {
|
||||
link_idx: self.link_idx.clone(),
|
||||
scope_id: self.scope_id.clone(),
|
||||
node: self.node,
|
||||
|
||||
+50
-173
@@ -1,7 +1,6 @@
|
||||
use crate::innerlude::*;
|
||||
|
||||
use futures_channel::mpsc::UnboundedSender;
|
||||
use fxhash::FxHashMap;
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
@@ -44,6 +43,8 @@ pub type Context<'a> = &'a Scope;
|
||||
/// use case they might have.
|
||||
pub struct Scope {
|
||||
pub(crate) parent_scope: Option<*mut Scope>,
|
||||
|
||||
// parent element I think?
|
||||
pub(crate) container: ElementId,
|
||||
|
||||
pub(crate) our_arena_idx: ScopeId,
|
||||
@@ -62,7 +63,9 @@ pub struct Scope {
|
||||
|
||||
pub(crate) items: RefCell<SelfReferentialItems<'static>>,
|
||||
|
||||
pub(crate) hooks: HookList,
|
||||
pub(crate) hook_arena: Bump,
|
||||
pub(crate) hook_vals: RefCell<SmallVec<[*mut dyn Any; 5]>>,
|
||||
pub(crate) hook_idx: Cell<usize>,
|
||||
|
||||
pub(crate) shared_contexts: RefCell<HashMap<TypeId, Rc<dyn Any>>>,
|
||||
|
||||
@@ -72,15 +75,13 @@ pub struct Scope {
|
||||
pub struct SelfReferentialItems<'a> {
|
||||
pub(crate) listeners: Vec<&'a Listener<'a>>,
|
||||
pub(crate) borrowed_props: Vec<&'a VComponent<'a>>,
|
||||
pub(crate) suspended_nodes: FxHashMap<u64, &'a VSuspended>,
|
||||
pub(crate) tasks: Vec<BumpBox<'a, dyn Future<Output = ()>>>,
|
||||
pub(crate) pending_effects: Vec<BumpBox<'a, dyn FnMut()>>,
|
||||
}
|
||||
|
||||
/// A component's unique identifier.
|
||||
///
|
||||
/// `ScopeId` is a `usize` that is unique across the entire VirtualDOM - but not unique across time. If a component is
|
||||
/// unmounted, then the `ScopeId` will be reused for a new component.
|
||||
/// `ScopeId` is a `usize` that is unique across the entire VirtualDOM and across time. ScopeIDs will never be reused
|
||||
/// once a component has been unmounted.
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
||||
pub struct ScopeId(pub usize);
|
||||
@@ -103,7 +104,9 @@ impl Scope {
|
||||
///
|
||||
/// assert_eq!(base.subtree(), 0);
|
||||
/// ```
|
||||
pub fn subtree(&self) -> u32 {
|
||||
///
|
||||
/// todo: enable
|
||||
pub(crate) fn _subtree(&self) -> u32 {
|
||||
self.subtree.get()
|
||||
}
|
||||
|
||||
@@ -122,14 +125,13 @@ impl Scope {
|
||||
/// rsx!(cx, div { "Subtree {id}"})
|
||||
/// };
|
||||
/// ```
|
||||
pub fn create_subtree(&self) -> Option<u32> {
|
||||
///
|
||||
/// todo: enable subtree
|
||||
pub(crate) fn _create_subtree(&self) -> Option<u32> {
|
||||
if self.is_subtree_root.get() {
|
||||
None
|
||||
} else {
|
||||
todo!()
|
||||
// let cur = self.subtree().get();
|
||||
// self.shared.cur_subtree.set(cur + 1);
|
||||
// Some(cur)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +142,7 @@ impl Scope {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// let mut dom = VirtualDom::new(|cx, props|cx.render(rsx!{ div {} }));
|
||||
/// let mut dom = VirtualDom::new(|cx, props| cx.render(rsx!{ div {} }));
|
||||
/// dom.rebuild();
|
||||
///
|
||||
/// let base = dom.base_scope();
|
||||
@@ -160,7 +162,7 @@ impl Scope {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// let mut dom = VirtualDom::new(|cx, props|cx.render(rsx!{ div {} }));
|
||||
/// let mut dom = VirtualDom::new(|cx, props| cx.render(rsx!{ div {} }));
|
||||
/// dom.rebuild();
|
||||
///
|
||||
/// let base = dom.base_scope();
|
||||
@@ -168,6 +170,7 @@ impl Scope {
|
||||
/// assert_eq!(base.parent(), None);
|
||||
/// ```
|
||||
pub fn parent(&self) -> Option<ScopeId> {
|
||||
// safety: the pointer to our parent is *always* valid thanks to the bump arena
|
||||
self.parent_scope.map(|p| unsafe { &*p }.our_arena_idx)
|
||||
}
|
||||
|
||||
@@ -178,7 +181,7 @@ impl Scope {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// let mut dom = VirtualDom::new(|cx, props|cx.render(rsx!{ div {} }));
|
||||
/// let mut dom = VirtualDom::new(|cx, props| cx.render(rsx!{ div {} }));
|
||||
/// dom.rebuild();
|
||||
/// let base = dom.base_scope();
|
||||
///
|
||||
@@ -192,11 +195,8 @@ impl Scope {
|
||||
///
|
||||
/// ## Notice: you should prefer using prepare_update and get_scope_id
|
||||
pub fn schedule_update(&self) -> Rc<dyn Fn() + 'static> {
|
||||
// pub fn schedule_update(&self) -> Rc<dyn Fn() + 'static> {
|
||||
let chan = self.sender.clone();
|
||||
let id = self.scope_id();
|
||||
let (chan, id) = (self.sender.clone(), self.scope_id());
|
||||
Rc::new(move || {
|
||||
log::debug!("set on channel an update for scope {:?}", id);
|
||||
let _ = chan.unbounded_send(SchedulerMsg::Immediate(id));
|
||||
})
|
||||
}
|
||||
@@ -284,25 +284,11 @@ impl Scope {
|
||||
}
|
||||
}
|
||||
|
||||
/// Push an effect to be ran after the component has been successfully mounted to the dom
|
||||
/// Returns the effect's position in the stack
|
||||
pub fn push_effect<'src>(&'src self, effect: impl FnOnce() + 'src) -> usize {
|
||||
// this is some tricker to get around not being able to actually call fnonces
|
||||
let mut slot = Some(effect);
|
||||
let fut: &mut dyn FnMut() = self.bump().alloc(move || slot.take().unwrap()());
|
||||
|
||||
// wrap it in a type that will actually drop the contents
|
||||
let boxed_fut = unsafe { BumpBox::from_raw(fut) };
|
||||
|
||||
// erase the 'src lifetime for self-referential storage
|
||||
let self_ref_fut = unsafe { std::mem::transmute(boxed_fut) };
|
||||
|
||||
let mut items = self.items.borrow_mut();
|
||||
items.pending_effects.push(self_ref_fut);
|
||||
items.pending_effects.len() - 1
|
||||
}
|
||||
|
||||
/// Pushes the future onto the poll queue to be polled
|
||||
/// Pushes the future onto the poll queue to be polled after the component renders.
|
||||
///
|
||||
///
|
||||
///
|
||||
///
|
||||
/// The future is forcibly dropped if the component is not ready by the next render
|
||||
pub fn push_task<'src, F: Future<Output = ()>>(
|
||||
&'src self,
|
||||
@@ -313,7 +299,7 @@ impl Scope {
|
||||
F: 'src,
|
||||
{
|
||||
self.sender
|
||||
.unbounded_send(SchedulerMsg::TaskPushed(self.our_arena_idx))
|
||||
.unbounded_send(SchedulerMsg::NewTask(self.our_arena_idx))
|
||||
.unwrap();
|
||||
|
||||
// allocate the future
|
||||
@@ -347,7 +333,7 @@ impl Scope {
|
||||
/// cx.render(lazy_tree)
|
||||
/// }
|
||||
///```
|
||||
pub fn render<'src>(&'src self, rsx: Option<LazyNodes<'src, '_>>) -> Option<NodeLink> {
|
||||
pub fn render<'src>(&'src self, rsx: Option<LazyNodes<'src, '_>>) -> Option<VPortal> {
|
||||
let frame = self.wip_frame();
|
||||
let bump = &frame.bump;
|
||||
let factory = NodeFactory { bump };
|
||||
@@ -357,7 +343,7 @@ impl Scope {
|
||||
let node_ptr = node as *mut _;
|
||||
let node_ptr = unsafe { std::mem::transmute(node_ptr) };
|
||||
|
||||
let link = NodeLink {
|
||||
let link = VPortal {
|
||||
scope_id: Cell::new(Some(self.our_arena_idx)),
|
||||
link_idx: Cell::new(0),
|
||||
node: node_ptr,
|
||||
@@ -366,31 +352,6 @@ impl Scope {
|
||||
Some(link)
|
||||
}
|
||||
|
||||
pub fn suspend<'src, F: Future<Output = Element> + 'src>(
|
||||
&'src self,
|
||||
mut fut: impl FnMut() -> F,
|
||||
) -> Option<VNode> {
|
||||
let channel = self.sender.clone();
|
||||
let node_fut = fut();
|
||||
|
||||
let scope = self.scope_id();
|
||||
|
||||
// self.push_task(move || {
|
||||
//
|
||||
// async move {
|
||||
// //
|
||||
// let r = node_fut.await;
|
||||
// if let Some(node) = r {
|
||||
// channel
|
||||
// .unbounded_send(SchedulerMsg::Suspended { node, scope })
|
||||
// .unwrap();
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Store a value between renders
|
||||
///
|
||||
/// This is *the* foundational hook for all other hooks.
|
||||
@@ -417,24 +378,35 @@ impl Scope {
|
||||
initializer: impl FnOnce(usize) -> State,
|
||||
runner: impl FnOnce(&'src mut State) -> Output,
|
||||
) -> Output {
|
||||
if self.hooks.at_end() {
|
||||
self.hooks.push_hook(initializer(self.hooks.len()));
|
||||
let mut vals = self.hook_vals.borrow_mut();
|
||||
let hook_len = vals.len();
|
||||
let cur_idx = self.hook_idx.get();
|
||||
|
||||
if cur_idx >= hook_len {
|
||||
let val = self.hook_arena.alloc(initializer(hook_len));
|
||||
vals.push(val);
|
||||
}
|
||||
|
||||
const HOOK_ERR_MSG: &str = r###"
|
||||
Unable to retrieve the hook that was initialized at this index.
|
||||
Consult the `rules of hooks` to understand how to use hooks properly.
|
||||
let state = vals
|
||||
.get(cur_idx)
|
||||
.and_then(|inn| {
|
||||
self.hook_idx.set(cur_idx + 1);
|
||||
let raw_box = unsafe { &mut **inn };
|
||||
raw_box.downcast_mut::<State>()
|
||||
})
|
||||
.expect(
|
||||
r###"
|
||||
Unable to retrieve the hook that was initialized at this index.
|
||||
Consult the `rules of hooks` to understand how to use hooks properly.
|
||||
|
||||
You likely used the hook in a conditional. Hooks rely on consistent ordering between renders.
|
||||
Functions prefixed with "use" should never be called conditionally.
|
||||
"###;
|
||||
You likely used the hook in a conditional. Hooks rely on consistent ordering between renders.
|
||||
Functions prefixed with "use" should never be called conditionally.
|
||||
"###,
|
||||
);
|
||||
|
||||
runner(self.hooks.next::<State>().expect(HOOK_ERR_MSG))
|
||||
runner(state)
|
||||
}
|
||||
}
|
||||
|
||||
// Important internal methods
|
||||
impl Scope {
|
||||
/// The "work in progress frame" represents the frame that is currently being worked on.
|
||||
pub(crate) fn wip_frame(&self) -> &BumpFrame {
|
||||
match self.generation.get() & 1 == 0 {
|
||||
@@ -471,25 +443,6 @@ impl Scope {
|
||||
self.generation.set(self.generation.get() + 1);
|
||||
}
|
||||
|
||||
// General strategy here is to load up the appropriate suspended task and then run it.
|
||||
// Suspended nodes cannot be called repeatedly.
|
||||
pub(crate) fn call_suspended_node<'a>(&'a mut self, task_id: u64) {
|
||||
let mut nodes = &mut self.items.get_mut().suspended_nodes;
|
||||
|
||||
if let Some(suspended) = nodes.remove(&task_id) {
|
||||
let sus: &'a VSuspended = suspended;
|
||||
// let mut boxed = sus.callback.borrow_mut().take().unwrap();
|
||||
// let new_node: Element = boxed();
|
||||
}
|
||||
}
|
||||
|
||||
// run the list of effects
|
||||
pub(crate) fn run_effects(&mut self) {
|
||||
for mut effect in self.items.get_mut().pending_effects.drain(..) {
|
||||
effect();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn root_node(&self) -> &VNode {
|
||||
let node = *self.wip_frame().nodes.borrow().get(0).unwrap();
|
||||
unsafe { std::mem::transmute(&*node) }
|
||||
@@ -514,11 +467,7 @@ impl BumpFrame {
|
||||
Self { bump, nodes }
|
||||
}
|
||||
|
||||
pub fn allocated_bytes(&self) -> usize {
|
||||
self.bump.allocated_bytes()
|
||||
}
|
||||
|
||||
pub fn assign_nodelink(&self, node: &NodeLink) {
|
||||
pub fn assign_nodelink(&self, node: &VPortal) {
|
||||
let mut nodes = self.nodes.borrow_mut();
|
||||
|
||||
let len = nodes.len();
|
||||
@@ -528,78 +477,6 @@ impl BumpFrame {
|
||||
}
|
||||
}
|
||||
|
||||
/// An abstraction over internally stored data using a hook-based memory layout.
|
||||
///
|
||||
/// Hooks are allocated using Boxes and then our stored references are given out.
|
||||
///
|
||||
/// It's unsafe to "reset" the hooklist, but it is safe to add hooks into it.
|
||||
///
|
||||
/// Todo: this could use its very own bump arena, but that might be a tad overkill
|
||||
#[derive(Default)]
|
||||
pub(crate) struct HookList {
|
||||
arena: Bump,
|
||||
vals: RefCell<SmallVec<[*mut dyn Any; 5]>>,
|
||||
idx: Cell<usize>,
|
||||
}
|
||||
|
||||
impl HookList {
|
||||
pub fn new(capacity: usize) -> Self {
|
||||
Self {
|
||||
arena: Bump::with_capacity(capacity),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn next<T: 'static>(&self) -> Option<&mut T> {
|
||||
self.vals.borrow().get(self.idx.get()).and_then(|inn| {
|
||||
self.idx.set(self.idx.get() + 1);
|
||||
let raw_box = unsafe { &mut **inn };
|
||||
raw_box.downcast_mut::<T>()
|
||||
})
|
||||
}
|
||||
|
||||
/// This resets the internal iterator count
|
||||
/// It's okay that we've given out each hook, but now we have the opportunity to give it out again
|
||||
/// Therefore, resetting is considered unsafe
|
||||
///
|
||||
/// This should only be ran by Dioxus itself before "running scope".
|
||||
/// Dioxus knows how to descend through the tree to prevent mutable aliasing.
|
||||
pub(crate) unsafe fn reset(&self) {
|
||||
self.idx.set(0);
|
||||
}
|
||||
|
||||
pub(crate) fn push_hook<T: 'static>(&self, new: T) {
|
||||
let val = self.arena.alloc(new);
|
||||
self.vals.borrow_mut().push(val)
|
||||
}
|
||||
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
self.vals.borrow().len()
|
||||
}
|
||||
|
||||
pub(crate) fn cur_idx(&self) -> usize {
|
||||
self.idx.get()
|
||||
}
|
||||
|
||||
pub(crate) fn at_end(&self) -> bool {
|
||||
self.cur_idx() >= self.len()
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.vals.borrow_mut().drain(..).for_each(|state| {
|
||||
let as_mut = unsafe { &mut *state };
|
||||
let boxed = unsafe { bumpalo::boxed::Box::from_raw(as_mut) };
|
||||
drop(boxed);
|
||||
});
|
||||
}
|
||||
|
||||
/// Get the ammount of memory a hooklist uses
|
||||
/// Used in heuristics
|
||||
pub fn get_hook_arena_size(&self) -> usize {
|
||||
self.arena.allocated_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sizeof() {
|
||||
dbg!(std::mem::size_of::<Scope>());
|
||||
|
||||
@@ -92,15 +92,15 @@ impl ScopeArena {
|
||||
let new_scope_id = ScopeId(self.scope_counter.get());
|
||||
self.scope_counter.set(self.scope_counter.get() + 1);
|
||||
|
||||
log::debug!("new scope {:?} with parent {:?}", new_scope_id, container);
|
||||
// log::debug!("new scope {:?} with parent {:?}", new_scope_id, container);
|
||||
|
||||
if let Some(old_scope) = self.free_scopes.borrow_mut().pop() {
|
||||
let scope = unsafe { &mut *old_scope };
|
||||
log::debug!(
|
||||
"reusing scope {:?} as {:?}",
|
||||
scope.our_arena_idx,
|
||||
new_scope_id
|
||||
);
|
||||
// log::debug!(
|
||||
// "reusing scope {:?} as {:?}",
|
||||
// scope.our_arena_idx,
|
||||
// new_scope_id
|
||||
// );
|
||||
|
||||
scope.caller = caller;
|
||||
scope.parent_scope = parent_scope;
|
||||
@@ -180,16 +180,17 @@ impl ScopeArena {
|
||||
caller,
|
||||
generation: 0.into(),
|
||||
|
||||
hooks: HookList::new(hook_capacity),
|
||||
shared_contexts: Default::default(),
|
||||
|
||||
items: RefCell::new(SelfReferentialItems {
|
||||
listeners: Default::default(),
|
||||
borrowed_props: Default::default(),
|
||||
suspended_nodes: Default::default(),
|
||||
tasks: Default::default(),
|
||||
pending_effects: Default::default(),
|
||||
}),
|
||||
|
||||
hook_arena: Bump::new(),
|
||||
hook_vals: RefCell::new(smallvec::SmallVec::with_capacity(hook_capacity)),
|
||||
hook_idx: Default::default(),
|
||||
});
|
||||
|
||||
let any_item = self.scopes.borrow_mut().insert(new_scope_id, scope);
|
||||
@@ -202,7 +203,7 @@ impl ScopeArena {
|
||||
pub fn try_remove(&self, id: &ScopeId) -> Option<()> {
|
||||
self.ensure_drop_safety(id);
|
||||
|
||||
log::debug!("removing scope {:?}", id);
|
||||
// log::debug!("removing scope {:?}", id);
|
||||
|
||||
// Safety:
|
||||
// - ensure_drop_safety ensures that no references to this scope are in use
|
||||
@@ -210,7 +211,14 @@ impl ScopeArena {
|
||||
let scope = unsafe { &mut *self.scopes.borrow_mut().remove(id).unwrap() };
|
||||
|
||||
// we're just reusing scopes so we need to clear it out
|
||||
scope.hooks.clear();
|
||||
scope.hook_vals.get_mut().drain(..).for_each(|state| {
|
||||
let as_mut = unsafe { &mut *state };
|
||||
let boxed = unsafe { bumpalo::boxed::Box::from_raw(as_mut) };
|
||||
drop(boxed);
|
||||
});
|
||||
scope.hook_idx.set(0);
|
||||
scope.hook_arena.reset();
|
||||
|
||||
scope.shared_contexts.get_mut().clear();
|
||||
scope.parent_scope = None;
|
||||
scope.generation.set(0);
|
||||
@@ -226,15 +234,11 @@ impl ScopeArena {
|
||||
let SelfReferentialItems {
|
||||
borrowed_props,
|
||||
listeners,
|
||||
pending_effects,
|
||||
suspended_nodes,
|
||||
tasks,
|
||||
} = scope.items.get_mut();
|
||||
|
||||
borrowed_props.clear();
|
||||
listeners.clear();
|
||||
pending_effects.clear();
|
||||
suspended_nodes.clear();
|
||||
tasks.clear();
|
||||
|
||||
self.free_scopes.borrow_mut().push(scope);
|
||||
@@ -311,12 +315,10 @@ impl ScopeArena {
|
||||
|
||||
let scope = unsafe { &mut *self.get_scope_mut(id).expect("could not find scope") };
|
||||
|
||||
log::debug!("found scope, about to run: {:?}", id);
|
||||
|
||||
// Safety:
|
||||
// - We dropped the listeners, so no more &mut T can be used while these are held
|
||||
// - All children nodes that rely on &mut T are replaced with a new reference
|
||||
unsafe { scope.hooks.reset() };
|
||||
scope.hook_idx.set(0);
|
||||
|
||||
// Safety:
|
||||
// - We've dropped all references to the wip bump frame with "ensure_drop_safety"
|
||||
@@ -326,16 +328,12 @@ impl ScopeArena {
|
||||
let mut items = scope.items.borrow_mut();
|
||||
|
||||
// just forget about our suspended nodes while we're at it
|
||||
items.suspended_nodes.clear();
|
||||
items.tasks.clear();
|
||||
items.pending_effects.clear();
|
||||
|
||||
// guarantee that we haven't screwed up - there should be no latent references anywhere
|
||||
debug_assert!(items.listeners.is_empty());
|
||||
debug_assert!(items.borrowed_props.is_empty());
|
||||
debug_assert!(items.suspended_nodes.is_empty());
|
||||
debug_assert!(items.tasks.is_empty());
|
||||
debug_assert!(items.pending_effects.is_empty());
|
||||
|
||||
// Todo: see if we can add stronger guarantees around internal bookkeeping and failed component renders.
|
||||
scope.wip_frame().nodes.borrow_mut().clear();
|
||||
|
||||
@@ -8,12 +8,9 @@ use futures_util::{Future, StreamExt};
|
||||
use fxhash::FxHashSet;
|
||||
use indexmap::IndexSet;
|
||||
use smallvec::SmallVec;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::task::Poll;
|
||||
use std::{any::Any, collections::VecDeque};
|
||||
use std::{any::Any, collections::VecDeque, pin::Pin, sync::Arc, task::Poll};
|
||||
|
||||
/// A virtual node system that progresses user events and diffs UI trees.
|
||||
/// A virtual node s ystem that progresses user events and diffs UI trees.
|
||||
///
|
||||
///
|
||||
/// ## Guide
|
||||
@@ -286,7 +283,7 @@ impl VirtualDom {
|
||||
if let Some(msg) = self.pending_messages.pop_back() {
|
||||
match msg {
|
||||
// just keep looping, the task is now saved but we should actually poll it
|
||||
SchedulerMsg::TaskPushed(id) => {
|
||||
SchedulerMsg::NewTask(id) => {
|
||||
self.scopes.pending_futures.borrow_mut().insert(id);
|
||||
}
|
||||
SchedulerMsg::UiEvent(event) => {
|
||||
@@ -297,7 +294,6 @@ impl VirtualDom {
|
||||
SchedulerMsg::Immediate(s) => {
|
||||
self.dirty_scopes.insert(s);
|
||||
}
|
||||
SchedulerMsg::Suspended { scope } => todo!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -352,7 +348,6 @@ impl VirtualDom {
|
||||
let mut committed_mutations = vec![];
|
||||
|
||||
while !self.dirty_scopes.is_empty() {
|
||||
log::debug!("working with deadline");
|
||||
let scopes = &self.scopes;
|
||||
let mut diff_state = DiffState::new(scopes);
|
||||
|
||||
@@ -361,6 +356,7 @@ impl VirtualDom {
|
||||
// Sort the scopes by height. Theoretically, we'll de-duplicate scopes by height
|
||||
self.dirty_scopes
|
||||
.retain(|id| scopes.get_scope(id).is_some());
|
||||
|
||||
self.dirty_scopes.sort_by(|a, b| {
|
||||
let h1 = scopes.get_scope(a).unwrap().height;
|
||||
let h2 = scopes.get_scope(b).unwrap().height;
|
||||
@@ -386,18 +382,16 @@ impl VirtualDom {
|
||||
}
|
||||
|
||||
if diff_state.work(&mut deadline) {
|
||||
let DiffState {
|
||||
mutations,
|
||||
seen_scopes,
|
||||
..
|
||||
} = diff_state;
|
||||
let DiffState { mutations, .. } = diff_state;
|
||||
|
||||
for scope in seen_scopes {
|
||||
self.dirty_scopes.remove(&scope);
|
||||
for scope in &mutations.dirty_scopes {
|
||||
self.dirty_scopes.remove(scope);
|
||||
}
|
||||
|
||||
committed_mutations.push(mutations);
|
||||
} else {
|
||||
log::debug!("Could not finish work in time");
|
||||
|
||||
// leave the work in an incomplete state
|
||||
return committed_mutations;
|
||||
}
|
||||
@@ -539,6 +533,7 @@ impl VirtualDom {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SchedulerMsg {
|
||||
// events from the host
|
||||
UiEvent(UserEvent),
|
||||
@@ -547,9 +542,7 @@ pub enum SchedulerMsg {
|
||||
Immediate(ScopeId),
|
||||
|
||||
// an async task pushed from an event handler (or just spawned)
|
||||
TaskPushed(ScopeId),
|
||||
|
||||
Suspended { scope: ScopeId },
|
||||
NewTask(ScopeId),
|
||||
}
|
||||
|
||||
/// User Events are events that are shuttled from the renderer into the VirtualDom trhough the scheduler channel.
|
||||
@@ -678,9 +671,9 @@ impl<'a> Future for PollTasks<'a> {
|
||||
// I think the futures neeed to be pinned using bumpbox or something
|
||||
// right now, they're bump allocated so this shouldn't matter anyway - they're not going to move
|
||||
let task_mut = task.as_mut();
|
||||
let unpinned = unsafe { Pin::new_unchecked(task_mut) };
|
||||
let pinned = unsafe { Pin::new_unchecked(task_mut) };
|
||||
|
||||
if unpinned.poll(cx).is_ready() {
|
||||
if pinned.poll(cx).is_ready() {
|
||||
all_pending = false
|
||||
} else {
|
||||
unfinished_tasks.push(task);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
//!
|
||||
//! It does not validated that component lifecycles work properly. This is done in another test file.
|
||||
|
||||
use dioxus::{prelude::*, DomEdit, VSuspended};
|
||||
use dioxus::{prelude::*, DomEdit};
|
||||
use dioxus_core as dioxus;
|
||||
use dioxus_core_macro::*;
|
||||
use dioxus_html as dioxus_elements;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "dioxus-coroutines"
|
||||
version = "0.0.0"
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
@@ -0,0 +1,8 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn it_works() {
|
||||
let result = 2 + 2;
|
||||
assert_eq!(result, 4);
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ thiserror = "1.0.23"
|
||||
log = "0.4.13"
|
||||
html-escape = "0.2.9"
|
||||
wry = "0.12.2"
|
||||
futures-channel = "0.3.16"
|
||||
futures-channel = "0.3.18"
|
||||
tokio = { version = "1.12.0", features = [
|
||||
"sync",
|
||||
"rt-multi-thread",
|
||||
|
||||
@@ -7,7 +7,7 @@ fn main() {
|
||||
dioxus::desktop::launch(App, |c| c)
|
||||
}
|
||||
|
||||
static App: FC<()> = |(cx, props)| {
|
||||
static App: FC<()> = |cx, props| {
|
||||
let (count, set_count) = use_state(cx, || 0);
|
||||
|
||||
cx.render(rsx!(
|
||||
@@ -34,7 +34,7 @@ Window management, system trays, notifications, and other desktop-related functi
|
||||
Managing windows is done by simply rendering content into a `WebviewWindow` component.
|
||||
|
||||
```rust
|
||||
static App: FC<()> = |(cx, props)| {
|
||||
static App: FC<()> = |cx, props| {
|
||||
rsx!(cx, WebviewWindow { "hello world" } )
|
||||
}
|
||||
```
|
||||
@@ -46,7 +46,7 @@ Notifications also use a declarative approach. Sending a notification has never
|
||||
The api has been somewhat modeled after https://github.com/mikaelbr/node-notifier
|
||||
|
||||
```rust
|
||||
static Notifications: FC<()> = |(cx, props)| {
|
||||
static Notifications: FC<()> = |cx, props| {
|
||||
cx.render(rsx!(
|
||||
Notification {
|
||||
title: "title"
|
||||
@@ -78,7 +78,7 @@ static Notifications: FC<()> = |(cx, props)| {
|
||||
Dioxus Desktop supports app trays, which can be built with native menu groups or with a custom window.
|
||||
|
||||
```rust
|
||||
static Tray: FC<()> = |(cx, props)| {
|
||||
static Tray: FC<()> = |cx, props| {
|
||||
cx.render(rsx!(
|
||||
GlobalTray {
|
||||
MenuGroup {
|
||||
@@ -90,7 +90,7 @@ static Tray: FC<()> = |(cx, props)| {
|
||||
};
|
||||
|
||||
// using a builder
|
||||
static Tray: FC<()> = |(cx, props)| {
|
||||
static Tray: FC<()> = |cx, props| {
|
||||
let menu = MenuGroup::builder(cx)
|
||||
.with_items([
|
||||
MenuGroupItem::builder()
|
||||
@@ -107,7 +107,7 @@ static Tray: FC<()> = |(cx, props)| {
|
||||
}
|
||||
|
||||
// or with a custom window
|
||||
static Tray: FC<()> = |(cx, props)| {
|
||||
static Tray: FC<()> = |cx, props| {
|
||||
rsx!(cx, GlobalTray { div { "custom buttons here" } })
|
||||
};
|
||||
```
|
||||
@@ -116,7 +116,7 @@ static Tray: FC<()> = |(cx, props)| {
|
||||
Declaring menus is convenient and cross-platform.
|
||||
|
||||
```rust
|
||||
static Menu: FC<()> = |(cx, props)| {
|
||||
static Menu: FC<()> = |cx, props| {
|
||||
cx.render(rsx!(
|
||||
MenuBarMajorItem { title: "File"
|
||||
MenuGroup {
|
||||
|
||||
@@ -11,14 +11,16 @@ use dioxus_hooks::*;
|
||||
use dioxus_html as dioxus_elements;
|
||||
|
||||
fn main() {
|
||||
simple_logger::init().unwrap();
|
||||
dioxus_desktop::launch(App, |c| c);
|
||||
}
|
||||
|
||||
static App: FC<()> = |cx, props| {
|
||||
let mut count = use_state(cx, || 0);
|
||||
log::debug!("count is {:?}", count);
|
||||
|
||||
cx.push_task(|| async move {
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
tokio::time::sleep(Duration::from_millis(1000)).await;
|
||||
count += 1;
|
||||
});
|
||||
|
||||
|
||||
+16
-12
@@ -1,14 +1,7 @@
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use dioxus_core::DomEdit;
|
||||
use wry::{
|
||||
application::{
|
||||
error::OsError,
|
||||
event_loop::{EventLoop, EventLoopWindowTarget},
|
||||
menu::MenuBar,
|
||||
window::{Fullscreen, Icon, Window, WindowBuilder},
|
||||
},
|
||||
webview::{RpcRequest, RpcResponse, WebView},
|
||||
application::{event_loop::EventLoop, window::WindowBuilder},
|
||||
webview::WebView,
|
||||
};
|
||||
|
||||
pub struct DesktopConfig<'a> {
|
||||
@@ -22,7 +15,7 @@ impl<'a> DesktopConfig<'a> {
|
||||
/// Initializes a new `WindowBuilder` with default values.
|
||||
#[inline]
|
||||
pub fn new() -> Self {
|
||||
let mut window = WindowBuilder::new().with_title("Dioxus app");
|
||||
let window = WindowBuilder::new().with_title("Dioxus app");
|
||||
Self {
|
||||
event_handler: None,
|
||||
window,
|
||||
@@ -41,13 +34,24 @@ impl<'a> DesktopConfig<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_window(&mut self, f: impl FnOnce(WindowBuilder) -> WindowBuilder) -> &mut Self {
|
||||
pub fn with_window(
|
||||
&mut self,
|
||||
configure: impl FnOnce(WindowBuilder) -> WindowBuilder,
|
||||
) -> &mut Self {
|
||||
// gots to do a swap because the window builder only takes itself as muy self
|
||||
// I wish more people knew about returning &mut Self
|
||||
let mut builder = WindowBuilder::default().with_title("Dioxus App");
|
||||
std::mem::swap(&mut self.window, &mut builder);
|
||||
builder = f(builder);
|
||||
builder = configure(builder);
|
||||
std::mem::swap(&mut self.window, &mut builder);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_event_handler(
|
||||
&mut self,
|
||||
handler: impl Fn(&mut EventLoop<()>, &mut WebView) + 'static,
|
||||
) -> &mut Self {
|
||||
self.event_handler = Some(Box::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
//! webview dom
|
||||
|
||||
use dioxus_core::DomEdit;
|
||||
|
||||
// pub struct WebviewRegistry {}
|
||||
|
||||
// impl WebviewRegistry {
|
||||
// pub fn new() -> Self {
|
||||
// Self {}
|
||||
// }
|
||||
// }
|
||||
|
||||
pub struct WebviewDom<'bump> {
|
||||
pub edits: Vec<DomEdit<'bump>>,
|
||||
pub node_counter: u64,
|
||||
// pub registry: WebviewRegistry,
|
||||
}
|
||||
impl WebviewDom<'_> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
edits: Vec::new(),
|
||||
node_counter: 0,
|
||||
// registry,
|
||||
}
|
||||
}
|
||||
|
||||
// // Finish using the dom (for its edit list) and give back the node and event registry
|
||||
// pub fn consume(self) -> WebviewRegistry {
|
||||
// self.registry
|
||||
// }
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -16,7 +16,8 @@ use std::fmt::{self, Write};
|
||||
///
|
||||
/// view.eval(&format!("callback({});", web_view::escape(string)));
|
||||
/// ```
|
||||
pub fn escape(string: &str) -> Escaper {
|
||||
#[allow(unused)]
|
||||
pub fn escape_js_string(string: &str) -> Escaper {
|
||||
Escaper(string)
|
||||
}
|
||||
|
||||
@@ -76,6 +77,6 @@ impl<'a> fmt::Display for Escaper<'a> {
|
||||
#[test]
|
||||
fn test() {
|
||||
let plain = "ABC \n\r' abc \\ \u{2028} \u{2029}123";
|
||||
let escaped = escape(plain).to_string();
|
||||
let escaped = escape_js_string(plain).to_string();
|
||||
assert!(escaped == "'ABC \\n\\r\\' abc \\\\ \\u2028 \\u2029123'");
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
//! Convert a serialized event to an event Trigger
|
||||
//!
|
||||
|
||||
use std::any::Any;
|
||||
use std::sync::Arc;
|
||||
use std::{any::Any, rc::Rc};
|
||||
|
||||
use dioxus_core::{ElementId, EventPriority, ScopeId, UserEvent};
|
||||
use dioxus_core::{ElementId, EventPriority, UserEvent};
|
||||
use dioxus_html::on::*;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
@@ -16,11 +16,11 @@ struct ImEvent {
|
||||
}
|
||||
|
||||
pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent {
|
||||
let mut ims: Vec<ImEvent> = serde_json::from_value(val).unwrap();
|
||||
let ims: Vec<ImEvent> = serde_json::from_value(val).unwrap();
|
||||
|
||||
let ImEvent {
|
||||
event,
|
||||
mounted_dom_id,
|
||||
// scope,
|
||||
contents,
|
||||
} = ims.into_iter().next().unwrap();
|
||||
|
||||
|
||||
@@ -19,10 +19,9 @@
|
||||
|
||||
|
||||
<body>
|
||||
<div id="_dioxusroot">
|
||||
<div id="main">
|
||||
</div>
|
||||
</body>
|
||||
<script type="text/javascript" src="index.js">
|
||||
</script>
|
||||
<script type="text/javascript" src="index.js"> </script>
|
||||
|
||||
</html>
|
||||
@@ -409,7 +409,7 @@ class Interpreter {
|
||||
}
|
||||
|
||||
function main() {
|
||||
let root = window.document.getElementById("_dioxusroot");
|
||||
let root = window.document.getElementById("main");
|
||||
window.interpreter = new Interpreter(root);
|
||||
console.log(window.interpreter);
|
||||
|
||||
|
||||
+181
-196
@@ -3,41 +3,31 @@
|
||||
//! Render the Dioxus VirtualDom using the platform's native WebView implementation.
|
||||
//!
|
||||
|
||||
use std::borrow::BorrowMut;
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::rc::Rc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::mpsc::channel;
|
||||
use std::sync::{Arc, RwLock};
|
||||
mod cfg;
|
||||
mod escape;
|
||||
mod events;
|
||||
// mod desktop_context;
|
||||
|
||||
use cfg::DesktopConfig;
|
||||
use dioxus_core::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub use wry;
|
||||
|
||||
use wry::application::accelerator::{Accelerator, SysMods};
|
||||
use wry::application::event::{ElementState, Event, StartCause, WindowEvent};
|
||||
use wry::application::event_loop::{self, ControlFlow, EventLoop, EventLoopWindowTarget};
|
||||
use wry::application::keyboard::{Key, KeyCode, ModifiersState};
|
||||
use wry::application::menu::{MenuBar, MenuItem, MenuItemAttributes};
|
||||
use wry::application::window::{Fullscreen, WindowId};
|
||||
use wry::webview::{WebView, WebViewBuilder};
|
||||
use std::{
|
||||
collections::{HashMap, VecDeque},
|
||||
sync::atomic::AtomicBool,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
use tokio::task::LocalSet;
|
||||
use wry::{
|
||||
application::menu,
|
||||
application::window::{Window, WindowBuilder},
|
||||
webview::{RpcRequest, RpcResponse},
|
||||
application::{
|
||||
accelerator::{Accelerator, SysMods},
|
||||
event::{Event, StartCause, WindowEvent},
|
||||
event_loop::{ControlFlow, EventLoop, EventLoopWindowTarget},
|
||||
keyboard::{KeyCode, ModifiersState},
|
||||
menu::{MenuBar, MenuItem},
|
||||
window::{Window, WindowId},
|
||||
},
|
||||
webview::RpcRequest,
|
||||
webview::{WebView, WebViewBuilder},
|
||||
};
|
||||
|
||||
mod cfg;
|
||||
mod desktop_context;
|
||||
mod dom;
|
||||
mod escape;
|
||||
mod events;
|
||||
|
||||
static HTML_CONTENT: &'static str = include_str!("./index.html");
|
||||
|
||||
pub fn launch(
|
||||
root: FC<()>,
|
||||
@@ -54,7 +44,7 @@ pub fn launch_with_props<P: Properties + 'static + Send + Sync>(
|
||||
run(root, props, builder)
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(serde::Serialize)]
|
||||
struct Response<'a> {
|
||||
pre_rendered: Option<String>,
|
||||
edits: Vec<DomEdit<'a>>,
|
||||
@@ -65,88 +55,43 @@ pub fn run<T: 'static + Send + Sync>(
|
||||
props: T,
|
||||
user_builder: impl for<'a, 'b> FnOnce(&'b mut DesktopConfig<'a>) -> &'b mut DesktopConfig<'a>,
|
||||
) {
|
||||
// Generate the config
|
||||
let mut cfg = DesktopConfig::new();
|
||||
user_builder(&mut cfg);
|
||||
let DesktopConfig {
|
||||
window: window_cfg,
|
||||
manual_edits,
|
||||
pre_rendered,
|
||||
..
|
||||
} = cfg;
|
||||
|
||||
// All of our webview windows are stored in a way that we can look them up later
|
||||
// The "DesktopContext" will provide functionality for spawning these windows
|
||||
let mut webviews = HashMap::<WindowId, WebView>::new();
|
||||
let event_loop = EventLoop::new();
|
||||
|
||||
let props_shared = Cell::new(Some(props));
|
||||
|
||||
// create local modifier state
|
||||
let modifiers = ModifiersState::default();
|
||||
let mut desktop_cfg = DesktopConfig::new();
|
||||
user_builder(&mut desktop_cfg);
|
||||
|
||||
let mut state = DesktopController::new_on_tokio(root, props);
|
||||
let quit_hotkey = Accelerator::new(SysMods::Cmd, KeyCode::KeyQ);
|
||||
|
||||
let edit_queue = Arc::new(RwLock::new(VecDeque::new()));
|
||||
let is_ready: Arc<AtomicBool> = Default::default();
|
||||
let modifiers = ModifiersState::default();
|
||||
let event_loop = EventLoop::new();
|
||||
|
||||
event_loop.run(move |window_event, event_loop, control_flow| {
|
||||
*control_flow = ControlFlow::Wait;
|
||||
|
||||
match window_event {
|
||||
Event::NewEvents(StartCause::Init) => {
|
||||
let window = create_window(event_loop, &window_cfg);
|
||||
let window_id = window.id();
|
||||
let sender =
|
||||
launch_vdom_with_tokio(root, props_shared.take().unwrap(), edit_queue.clone());
|
||||
let webview = create_webview(window, is_ready.clone(), sender);
|
||||
webviews.insert(window_id, webview);
|
||||
}
|
||||
Event::NewEvents(StartCause::Init) => state.new_window(&desktop_cfg, event_loop),
|
||||
|
||||
Event::WindowEvent {
|
||||
event, window_id, ..
|
||||
} => match event {
|
||||
WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
|
||||
WindowEvent::Destroyed { .. } => {
|
||||
webviews.remove(&window_id);
|
||||
if webviews.is_empty() {
|
||||
*control_flow = ControlFlow::Exit;
|
||||
}
|
||||
}
|
||||
WindowEvent::Moved(pos) => {
|
||||
//
|
||||
}
|
||||
WindowEvent::Destroyed { .. } => state.close_window(window_id, control_flow),
|
||||
|
||||
WindowEvent::KeyboardInput { event, .. } => {
|
||||
if quit_hotkey.matches(&modifiers, &event.physical_key) {
|
||||
webviews.remove(&window_id);
|
||||
if webviews.is_empty() {
|
||||
*control_flow = ControlFlow::Exit;
|
||||
}
|
||||
state.close_window(window_id, control_flow);
|
||||
}
|
||||
}
|
||||
|
||||
WindowEvent::Resized(_) | WindowEvent::Moved(_) => {
|
||||
if let Some(view) = webviews.get_mut(&window_id) {
|
||||
if let Some(view) = state.webviews.get_mut(&window_id) {
|
||||
let _ = view.resize();
|
||||
}
|
||||
}
|
||||
// TODO: we want to shuttle all of these events into the user's app
|
||||
|
||||
// TODO: we want to shuttle all of these events into the user's app or provide some handler
|
||||
_ => {}
|
||||
},
|
||||
|
||||
Event::MainEventsCleared => {
|
||||
// I hate this ready hack but it's needed to wait for the "onload" to occur
|
||||
// We can't run any initializion scripts because the window isn't ready yet?
|
||||
if is_ready.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
let mut queue = edit_queue.write().unwrap();
|
||||
let (id, view) = webviews.iter_mut().next().unwrap();
|
||||
while let Some(edit) = queue.pop_back() {
|
||||
view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::MainEventsCleared => state.try_load_ready_webviews(),
|
||||
Event::Resumed => {}
|
||||
Event::Suspended => {}
|
||||
Event::LoopDestroyed => {}
|
||||
@@ -156,120 +101,160 @@ pub fn run<T: 'static + Send + Sync>(
|
||||
})
|
||||
}
|
||||
|
||||
// Create a new tokio runtime on a dedicated thread and then launch the apps VirtualDom.
|
||||
pub(crate) fn launch_vdom_with_tokio<P: Send + 'static>(
|
||||
root: FC<P>,
|
||||
props: P,
|
||||
edit_queue: Arc<RwLock<VecDeque<String>>>,
|
||||
) -> futures_channel::mpsc::UnboundedSender<SchedulerMsg> {
|
||||
let (sender, receiver) = futures_channel::mpsc::unbounded::<SchedulerMsg>();
|
||||
let return_sender = sender.clone();
|
||||
pub struct DesktopController {
|
||||
webviews: HashMap<WindowId, WebView>,
|
||||
sender: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
|
||||
pending_edits: Arc<RwLock<VecDeque<String>>>,
|
||||
quit_app_on_close: bool,
|
||||
is_ready: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
std::thread::spawn(move || {
|
||||
// We create the runtim as multithreaded, so you can still "spawn" onto multiple threads
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
impl DesktopController {
|
||||
// Launch the virtualdom on its own thread managed by tokio
|
||||
// returns the desktop state
|
||||
pub fn new_on_tokio<P: Send + 'static>(root: FC<P>, props: P) -> Self {
|
||||
let edit_queue = Arc::new(RwLock::new(VecDeque::new()));
|
||||
let pending_edits = edit_queue.clone();
|
||||
|
||||
let (sender, receiver) = futures_channel::mpsc::unbounded::<SchedulerMsg>();
|
||||
let return_sender = sender.clone();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
// We create the runtim as multithreaded, so you can still "spawn" onto multiple threads
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
runtime.block_on(async move {
|
||||
// LocalSet::new().block_on(&runtime, async move {
|
||||
let mut dom =
|
||||
VirtualDom::new_with_props_and_scheduler(root, props, sender, receiver);
|
||||
|
||||
let edits = dom.rebuild();
|
||||
|
||||
edit_queue
|
||||
.write()
|
||||
.unwrap()
|
||||
.push_front(serde_json::to_string(&edits.edits).unwrap());
|
||||
|
||||
loop {
|
||||
dom.wait_for_work().await;
|
||||
let mut muts = dom.work_with_deadline(|| false);
|
||||
while let Some(edit) = muts.pop() {
|
||||
log::debug!("found mutations {:?}", muts);
|
||||
edit_queue
|
||||
.write()
|
||||
.unwrap()
|
||||
.push_front(serde_json::to_string(&edit.edits).unwrap());
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
Self {
|
||||
pending_edits,
|
||||
sender: return_sender,
|
||||
|
||||
webviews: HashMap::new(),
|
||||
is_ready: Arc::new(AtomicBool::new(false)),
|
||||
quit_app_on_close: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_window(&mut self, cfg: &DesktopConfig, event_loop: &EventLoopWindowTarget<()>) {
|
||||
let builder = cfg.window.clone().with_menu({
|
||||
// create main menubar menu
|
||||
let mut menu_bar_menu = MenuBar::new();
|
||||
|
||||
// create `first_menu`
|
||||
let mut first_menu = MenuBar::new();
|
||||
|
||||
first_menu.add_native_item(MenuItem::About("Todos".to_string()));
|
||||
first_menu.add_native_item(MenuItem::Services);
|
||||
first_menu.add_native_item(MenuItem::Separator);
|
||||
first_menu.add_native_item(MenuItem::Hide);
|
||||
first_menu.add_native_item(MenuItem::HideOthers);
|
||||
first_menu.add_native_item(MenuItem::ShowAll);
|
||||
|
||||
first_menu.add_native_item(MenuItem::Quit);
|
||||
first_menu.add_native_item(MenuItem::CloseWindow);
|
||||
|
||||
// create second menu
|
||||
let mut second_menu = MenuBar::new();
|
||||
|
||||
// second_menu.add_submenu("Sub menu", true, my_sub_menu);
|
||||
second_menu.add_native_item(MenuItem::Copy);
|
||||
second_menu.add_native_item(MenuItem::Paste);
|
||||
second_menu.add_native_item(MenuItem::SelectAll);
|
||||
|
||||
menu_bar_menu.add_submenu("First menu", true, first_menu);
|
||||
menu_bar_menu.add_submenu("Second menu", true, second_menu);
|
||||
|
||||
menu_bar_menu
|
||||
});
|
||||
|
||||
let window = builder.build(event_loop).unwrap();
|
||||
let window_id = window.id();
|
||||
|
||||
let (is_ready, sender) = (self.is_ready.clone(), self.sender.clone());
|
||||
|
||||
let webview = WebViewBuilder::new(window)
|
||||
.unwrap()
|
||||
.with_url("wry://index.html")
|
||||
.unwrap()
|
||||
.with_rpc_handler(move |_window: &Window, req: RpcRequest| {
|
||||
match req.method.as_str() {
|
||||
"user_event" => {
|
||||
let event = events::trigger_from_serialized(req.params.unwrap());
|
||||
log::debug!("User event: {:?}", event);
|
||||
sender.unbounded_send(SchedulerMsg::UiEvent(event)).unwrap();
|
||||
}
|
||||
"initialize" => {
|
||||
is_ready.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
// response always driven through eval.
|
||||
// unfortunately, it seems to be pretty slow, so we might want to look into an RPC form
|
||||
None
|
||||
})
|
||||
// Any content that that uses the `wry://` scheme will be shuttled through this handler as a "special case"
|
||||
// For now, we only serve two pieces of content which get included as bytes into the final binary.
|
||||
.with_custom_protocol("wry".into(), move |request| {
|
||||
let path = request.uri().replace("wry://", "");
|
||||
let (data, meta) = match path.as_str() {
|
||||
"index.html" => (include_bytes!("./index.html").to_vec(), "text/html"),
|
||||
"index.html/index.js" => {
|
||||
(include_bytes!("./index.js").to_vec(), "text/javascript")
|
||||
}
|
||||
_ => unimplemented!("path {}", path),
|
||||
};
|
||||
|
||||
wry::http::ResponseBuilder::new().mimetype(meta).body(data)
|
||||
})
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
runtime.block_on(async move {
|
||||
let mut dom = VirtualDom::new_with_props_and_scheduler(root, props, sender, receiver);
|
||||
self.webviews.insert(window_id, webview);
|
||||
}
|
||||
|
||||
let edits = dom.rebuild();
|
||||
pub fn close_window(&mut self, window_id: WindowId, control_flow: &mut ControlFlow) {
|
||||
self.webviews.remove(&window_id);
|
||||
|
||||
edit_queue
|
||||
.write()
|
||||
.unwrap()
|
||||
.push_front(serde_json::to_string(&edits.edits).unwrap());
|
||||
if self.webviews.is_empty() && self.quit_app_on_close {
|
||||
*control_flow = ControlFlow::Exit;
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
dom.wait_for_work().await;
|
||||
let mut muts = dom.work_with_deadline(|| false);
|
||||
while let Some(edit) = muts.pop() {
|
||||
edit_queue
|
||||
.write()
|
||||
.unwrap()
|
||||
.push_front(serde_json::to_string(&edit.edits).unwrap());
|
||||
}
|
||||
pub fn try_load_ready_webviews(&mut self) {
|
||||
if self.is_ready.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
let mut queue = self.pending_edits.write().unwrap();
|
||||
let (_id, view) = self.webviews.iter_mut().next().unwrap();
|
||||
while let Some(edit) = queue.pop_back() {
|
||||
view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit))
|
||||
.unwrap();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return_sender
|
||||
}
|
||||
|
||||
fn build_menu() -> MenuBar {
|
||||
// create main menubar menu
|
||||
let mut menu_bar_menu = MenuBar::new();
|
||||
|
||||
// create `first_menu`
|
||||
let mut first_menu = MenuBar::new();
|
||||
|
||||
first_menu.add_native_item(MenuItem::About("Todos".to_string()));
|
||||
first_menu.add_native_item(MenuItem::Services);
|
||||
first_menu.add_native_item(MenuItem::Separator);
|
||||
first_menu.add_native_item(MenuItem::Hide);
|
||||
first_menu.add_native_item(MenuItem::HideOthers);
|
||||
first_menu.add_native_item(MenuItem::ShowAll);
|
||||
|
||||
first_menu.add_native_item(MenuItem::Quit);
|
||||
first_menu.add_native_item(MenuItem::CloseWindow);
|
||||
|
||||
// create second menu
|
||||
let mut second_menu = MenuBar::new();
|
||||
|
||||
// second_menu.add_submenu("Sub menu", true, my_sub_menu);
|
||||
second_menu.add_native_item(MenuItem::Copy);
|
||||
second_menu.add_native_item(MenuItem::Paste);
|
||||
second_menu.add_native_item(MenuItem::SelectAll);
|
||||
|
||||
menu_bar_menu.add_submenu("First menu", true, first_menu);
|
||||
menu_bar_menu.add_submenu("Second menu", true, second_menu);
|
||||
|
||||
menu_bar_menu
|
||||
}
|
||||
|
||||
fn create_window(event_loop: &EventLoopWindowTarget<()>, cfg: &WindowBuilder) -> Window {
|
||||
let builder = cfg.clone().with_menu(build_menu());
|
||||
builder.build(event_loop).unwrap()
|
||||
}
|
||||
|
||||
fn create_webview(
|
||||
window: Window,
|
||||
is_ready: Arc<AtomicBool>,
|
||||
sender: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
|
||||
) -> WebView {
|
||||
WebViewBuilder::new(window)
|
||||
.unwrap()
|
||||
.with_url("wry://index.html")
|
||||
.unwrap()
|
||||
.with_rpc_handler(move |_window: &Window, mut req: RpcRequest| {
|
||||
match req.method.as_str() {
|
||||
"user_event" => {
|
||||
let event = events::trigger_from_serialized(req.params.unwrap());
|
||||
log::debug!("User event: {:?}", event);
|
||||
sender.unbounded_send(SchedulerMsg::UiEvent(event)).unwrap();
|
||||
}
|
||||
"initialize" => {
|
||||
is_ready.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
// always driven through eval
|
||||
None
|
||||
})
|
||||
// Any content that that uses the `wry://` scheme will be shuttled through this handler as a "special case"
|
||||
// For now, we only serve two pieces of content which get included as bytes into the final binary.
|
||||
.with_custom_protocol("wry".into(), move |request| {
|
||||
let path = request.uri().replace("wry://", "");
|
||||
let (data, meta) = match path.as_str() {
|
||||
"index.html" => (include_bytes!("./index.html").to_vec(), "text/html"),
|
||||
"index.html/index.js" => (include_bytes!("./index.js").to_vec(), "text/javascript"),
|
||||
_ => unimplemented!("path {}", path),
|
||||
};
|
||||
|
||||
wry::http::ResponseBuilder::new().mimetype(meta).body(data)
|
||||
})
|
||||
.build()
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,4 +7,5 @@ edition = "2018"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dioxus-core = { path = "../../packages/core", version = "0.1.2" }
|
||||
dioxus-core = { path = "../../packages/core", version = "0.1.3" }
|
||||
futures = "0.3.18"
|
||||
@@ -19,7 +19,7 @@ You can always use it "normally" with the `split` method:
|
||||
let value = use_state(cx, || 10);
|
||||
|
||||
// "Classic" usage:
|
||||
let (value, set_value) = use_state(cx, || 0).classic();
|
||||
let (value, set_value) = use_state(cx, || 0).split();
|
||||
```
|
||||
|
||||
## use_ref
|
||||
|
||||
@@ -6,3 +6,6 @@ pub use useref::*;
|
||||
|
||||
mod use_shared_state;
|
||||
pub use use_shared_state::*;
|
||||
|
||||
mod usecoroutine;
|
||||
pub use usecoroutine::*;
|
||||
@@ -173,6 +173,6 @@ pub fn use_provide_state<'a, T: 'static>(cx: Context<'a>, f: impl FnOnce() -> T)
|
||||
});
|
||||
cx.provide_state(state)
|
||||
},
|
||||
|inner| {},
|
||||
|_inner| {},
|
||||
)
|
||||
}
|
||||
@@ -36,7 +36,7 @@ uses the same memoization on top of the use_context API.
|
||||
|
||||
Here's a fully-functional todo app using the use_map API:
|
||||
```rust
|
||||
static TodoList: FC<()> = |(cx, props)|{
|
||||
static TodoList: FC<()> = |cx, props|{
|
||||
let todos = use_map(cx, || HashMap::new());
|
||||
let input = use_ref(|| None);
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
use dioxus_core::Context;
|
||||
use futures::Future;
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
pub fn use_coroutine<'a, F: Future<Output = ()> + 'a>(
|
||||
cx: Context<'a>,
|
||||
f: impl FnOnce() -> F + 'a,
|
||||
) -> CoroutineHandle {
|
||||
//
|
||||
cx.use_hook(
|
||||
move |_| State {
|
||||
running: Default::default(),
|
||||
fut: Default::default(),
|
||||
submit: Default::default(),
|
||||
},
|
||||
|state| {
|
||||
let fut_slot = state.fut.clone();
|
||||
let running = state.running.clone();
|
||||
let submit: Box<dyn FnOnce() + 'a> = Box::new(move || {
|
||||
let g = async move {
|
||||
running.set(true);
|
||||
f().await;
|
||||
running.set(false);
|
||||
};
|
||||
let p: Pin<Box<dyn Future<Output = ()>>> = Box::pin(g);
|
||||
fut_slot
|
||||
.borrow_mut()
|
||||
.replace(unsafe { std::mem::transmute(p) });
|
||||
});
|
||||
|
||||
let submit = unsafe { std::mem::transmute(submit) };
|
||||
state.submit.get_mut().replace(submit);
|
||||
|
||||
if state.running.get() {
|
||||
let mut fut = state.fut.borrow_mut();
|
||||
cx.push_task(|| fut.as_mut().unwrap().as_mut());
|
||||
} else {
|
||||
// make sure to drop the old future
|
||||
if let Some(fut) = state.fut.borrow_mut().take() {
|
||||
drop(fut);
|
||||
}
|
||||
}
|
||||
CoroutineHandle { cx, inner: state }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
struct State {
|
||||
running: Rc<Cell<bool>>,
|
||||
submit: RefCell<Option<Box<dyn FnOnce()>>>,
|
||||
fut: Rc<RefCell<Option<Pin<Box<dyn Future<Output = ()>>>>>>,
|
||||
}
|
||||
|
||||
pub struct CoroutineHandle<'a> {
|
||||
cx: Context<'a>,
|
||||
inner: &'a State,
|
||||
}
|
||||
|
||||
impl<'a> CoroutineHandle<'a> {
|
||||
pub fn start(&self) {
|
||||
if self.inner.running.get() {
|
||||
return;
|
||||
}
|
||||
if let Some(submit) = self.inner.submit.borrow_mut().take() {
|
||||
submit();
|
||||
let mut fut = self.inner.fut.borrow_mut();
|
||||
self.cx.push_task(|| fut.as_mut().unwrap().as_mut());
|
||||
}
|
||||
}
|
||||
pub fn resume(&self) {}
|
||||
}
|
||||
@@ -33,6 +33,11 @@ impl<'a, T> UseRef<'a, T> {
|
||||
self.inner.value.borrow()
|
||||
}
|
||||
|
||||
pub fn set(&self, new: T) {
|
||||
*self.inner.value.borrow_mut() = new;
|
||||
self.needs_update();
|
||||
}
|
||||
|
||||
pub fn read_write(&self) -> (Ref<'_, T>, &Self) {
|
||||
(self.read(), self)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use dioxus_core::prelude::Context;
|
||||
use std::{
|
||||
cell::{Cell, Ref, RefCell, RefMut},
|
||||
fmt::Display,
|
||||
fmt::{Debug, Display},
|
||||
ops::Not,
|
||||
rc::Rc,
|
||||
};
|
||||
@@ -35,7 +35,7 @@ use std::{
|
||||
///
|
||||
/// Usage:
|
||||
/// ```ignore
|
||||
/// const Example: FC<()> = |(cx, props)|{
|
||||
/// const Example: FC<()> = |cx, props|{
|
||||
/// let counter = use_state(cx, || 0);
|
||||
/// let increment = |_| counter += 1;
|
||||
/// let decrement = |_| counter += 1;
|
||||
@@ -54,11 +54,14 @@ pub fn use_state<'a, T: 'static>(
|
||||
initial_state_fn: impl FnOnce() -> T,
|
||||
) -> UseState<'a, T> {
|
||||
cx.use_hook(
|
||||
move |_| UseStateInner {
|
||||
current_val: initial_state_fn(),
|
||||
update_callback: cx.schedule_update(),
|
||||
wip: Rc::new(RefCell::new(None)),
|
||||
update_scheuled: Cell::new(false),
|
||||
move |_| {
|
||||
//
|
||||
UseStateInner {
|
||||
current_val: initial_state_fn(),
|
||||
update_callback: cx.schedule_update(),
|
||||
wip: Rc::new(RefCell::new(None)),
|
||||
update_scheuled: Cell::new(false),
|
||||
}
|
||||
},
|
||||
move |hook| {
|
||||
hook.update_scheuled.set(false);
|
||||
@@ -91,7 +94,11 @@ where
|
||||
UseState { inner: self.inner }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Debug> Debug for UseState<'_, T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self.inner.current_val)
|
||||
}
|
||||
}
|
||||
impl<'a, T: 'static> UseState<'a, T> {
|
||||
/// Tell the Dioxus Scheduler that we need to be processed
|
||||
pub fn needs_update(&self) {
|
||||
|
||||
@@ -717,8 +717,7 @@ builder_constructors! {
|
||||
nonce: Nonce,
|
||||
src: Uri,
|
||||
text: String,
|
||||
r#async: Bool,
|
||||
r#type: String, // TODO could be an enum
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -823,7 +822,6 @@ builder_constructors! {
|
||||
formnovalidate: Bool,
|
||||
formtarget: Target,
|
||||
name: Id,
|
||||
r#type: ButtonType,
|
||||
value: String,
|
||||
};
|
||||
|
||||
@@ -1064,6 +1062,23 @@ impl input {
|
||||
volatile attributes
|
||||
*/
|
||||
|
||||
impl script {
|
||||
// r#async: Bool,
|
||||
// r#type: String, // TODO could be an enum
|
||||
pub fn r#type<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> {
|
||||
cx.attr("type", val, None, false)
|
||||
}
|
||||
pub fn r#script<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> {
|
||||
cx.attr("script", val, None, false)
|
||||
}
|
||||
}
|
||||
|
||||
impl button {
|
||||
pub fn r#type<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> {
|
||||
cx.attr("type", val, None, false)
|
||||
}
|
||||
}
|
||||
|
||||
impl select {
|
||||
pub fn value<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> {
|
||||
cx.attr("value", val, None, true)
|
||||
|
||||
@@ -22,7 +22,7 @@ web-sys = { version = "0.3", features = [
|
||||
"Window",
|
||||
], optional = true }
|
||||
|
||||
serde = "1"
|
||||
serde = "1.0.130"
|
||||
|
||||
serde_urlencoded = "0.7"
|
||||
|
||||
@@ -30,10 +30,19 @@ wasm-bindgen = "0.2"
|
||||
js-sys = "0.3"
|
||||
gloo = "0.4"
|
||||
route-recognizer = "0.3.1"
|
||||
url = "2.2.2"
|
||||
url_serde = "0.2.0"
|
||||
|
||||
|
||||
[features]
|
||||
default = ["web"]
|
||||
default = ["web", "derive"]
|
||||
web = ["web-sys"]
|
||||
desktop = []
|
||||
mobile = []
|
||||
derive = []
|
||||
|
||||
[dev-dependencies]
|
||||
console_error_panic_hook = "0.1.7"
|
||||
dioxus-web = { path = "../web" }
|
||||
log = "0.4.14"
|
||||
wasm-logger = "0.2.0"
|
||||
@@ -0,0 +1,40 @@
|
||||
# Router hook for Dioxus apps
|
||||
|
||||
Dioxus-router provides a use_router hook that returns a different value depending on the route.
|
||||
The router is generic over any value, however it makes sense to return a different set of VNodes
|
||||
and feed them into the App's return VNodes.
|
||||
|
||||
Using the router should feel similar to tide's routing framework where an "address" book is assembled at the head of the app.
|
||||
|
||||
Here's an example of how to use the router hook:
|
||||
|
||||
```rust
|
||||
#[derive(Clone, PartialEq, Serialize, Deserialize, Routable)]
|
||||
enum AppRoute {
|
||||
Home,
|
||||
Posts,
|
||||
NotFound
|
||||
}
|
||||
|
||||
static App: FC<()> = |cx, props| {
|
||||
let route = use_router(cx, AppRoute::parse);
|
||||
|
||||
match route {
|
||||
AppRoute::Home => rsx!(cx, Home {})
|
||||
AppRoute::Posts => rsx!(cx, Posts {})
|
||||
AppRoute::Notfound => rsx!(cx, Notfound {})
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Adding links into your app:
|
||||
|
||||
```rust
|
||||
static Leaf: FC<()> = |cx, props| {
|
||||
rsx!(cx, div {
|
||||
Link { to: AppRoute::Home }
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Currently, the router is only supported in a web environment, but we plan to add 1st-party support via the context API when new renderers are available.
|
||||
@@ -0,0 +1,39 @@
|
||||
use dioxus_core::prelude::*;
|
||||
use dioxus_core_macro::*;
|
||||
use dioxus_html as dioxus_elements;
|
||||
use dioxus_router::*;
|
||||
|
||||
fn main() {
|
||||
console_error_panic_hook::set_once();
|
||||
wasm_logger::init(wasm_logger::Config::new(log::Level::Debug));
|
||||
dioxus_web::launch(App, |c| c);
|
||||
}
|
||||
|
||||
static App: FC<()> = |cx, props| {
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
enum Route {
|
||||
Home,
|
||||
About,
|
||||
NotFound,
|
||||
}
|
||||
|
||||
let route = use_router(cx, |s| match s {
|
||||
"/" => Route::Home,
|
||||
"/about" => Route::About,
|
||||
_ => Route::NotFound,
|
||||
});
|
||||
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
{match route {
|
||||
Route::Home => rsx!(h1 { "Home" }),
|
||||
Route::About => rsx!(h1 { "About" }),
|
||||
Route::NotFound => rsx!(h1 { "NotFound" }),
|
||||
}}
|
||||
nav {
|
||||
Link { to: Route::Home, href: |_| "/".to_string() }
|
||||
Link { to: Route::About, href: |_| "/about".to_string() }
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
+78
-40
@@ -1,24 +1,29 @@
|
||||
mod utils;
|
||||
|
||||
use std::{cell::RefCell, collections::HashMap, rc::Rc};
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use dioxus_core as dioxus;
|
||||
use dioxus_core::prelude::*;
|
||||
use dioxus_core_macro::{format_args_f, rsx, Props};
|
||||
use dioxus_core_macro::{rsx, Props};
|
||||
use dioxus_html as dioxus_elements;
|
||||
use wasm_bindgen::JsValue;
|
||||
use web_sys::Event;
|
||||
use wasm_bindgen::{JsCast, JsValue};
|
||||
use web_sys::{window, Event};
|
||||
|
||||
use crate::utils::fetch_base_url;
|
||||
use crate::utils::strip_slash_suffix;
|
||||
|
||||
pub trait Routable: 'static + Send + Clone + PartialEq {}
|
||||
impl<T> Routable for T where T: 'static + Send + Clone + PartialEq {}
|
||||
|
||||
pub struct RouterService<R: Routable> {
|
||||
history: RefCell<Vec<R>>,
|
||||
historic_routes: Vec<R>,
|
||||
history_service: RefCell<web_sys::History>,
|
||||
base_ur: RefCell<Option<String>>,
|
||||
}
|
||||
|
||||
impl<R: Routable> RouterService<R> {
|
||||
fn push_route(&self, r: R) {
|
||||
self.history.borrow_mut().push(r);
|
||||
todo!()
|
||||
// self.historic_routes.borrow_mut().push(r);
|
||||
}
|
||||
|
||||
fn get_current_route(&self) -> &str {
|
||||
@@ -61,38 +66,71 @@ impl<R: Routable> RouterService<R> {
|
||||
/// This hould only be used once per app
|
||||
///
|
||||
/// You can manually parse the route if you want, but the derived `parse` method on `Routable` will also work just fine
|
||||
pub fn use_router<R: Routable>(cx: Context, cfg: impl FnOnce(&str) -> R) -> Option<&R> {
|
||||
pub fn use_router<R: Routable>(cx: Context, mut parse: impl FnMut(&str) -> R + 'static) -> &R {
|
||||
// for the web, attach to the history api
|
||||
cx.use_hook(
|
||||
|f| {
|
||||
//
|
||||
use gloo::events::EventListener;
|
||||
|
||||
let base_url = fetch_base_url();
|
||||
let base = window()
|
||||
.unwrap()
|
||||
.document()
|
||||
.unwrap()
|
||||
.query_selector("base[href]")
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|base| {
|
||||
let base = JsCast::unchecked_into::<web_sys::HtmlBaseElement>(base).href();
|
||||
let url = web_sys::Url::new(&base).unwrap();
|
||||
|
||||
if url.pathname() != "/" {
|
||||
Some(strip_slash_suffix(&base).to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let location = window().unwrap().location();
|
||||
let pathname = location.pathname().unwrap();
|
||||
let initial_route = parse(&pathname);
|
||||
|
||||
let service: RouterService<R> = RouterService {
|
||||
history: RefCell::new(vec![]),
|
||||
base_ur: RefCell::new(base_url),
|
||||
historic_routes: vec![initial_route],
|
||||
history_service: RefCell::new(
|
||||
web_sys::window().unwrap().history().expect("no history"),
|
||||
),
|
||||
base_ur: RefCell::new(base),
|
||||
};
|
||||
|
||||
cx.provide_state(service);
|
||||
// let base = base_url();
|
||||
// let url = route.to_path();
|
||||
// pending_routes: RefCell::new(vec![]),
|
||||
// service.history_service.push_state(data, title);
|
||||
|
||||
// cx.provide_state(service);
|
||||
|
||||
let regenerate = cx.schedule_update();
|
||||
|
||||
// when "back" is called by the user, we want to to re-render the component
|
||||
// // when "back" is called by the user, we want to to re-render the component
|
||||
let listener = EventListener::new(&web_sys::window().unwrap(), "popstate", move |_| {
|
||||
//
|
||||
regenerate();
|
||||
});
|
||||
},
|
||||
|f| {
|
||||
//
|
||||
},
|
||||
);
|
||||
|
||||
todo!()
|
||||
// let router = use_router_service::<R>(cx)?;
|
||||
// Some(cfg(router.get_current_route()))
|
||||
service
|
||||
},
|
||||
|state| {
|
||||
let base = state.base_ur.borrow();
|
||||
if let Some(base) = base.as_ref() {
|
||||
//
|
||||
let path = format!("{}{}", base, state.get_current_route());
|
||||
}
|
||||
let history = state.history_service.borrow();
|
||||
|
||||
state.historic_routes.last().unwrap()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn use_router_service<R: Routable>(cx: Context) -> Option<&Rc<RouterService<R>>> {
|
||||
@@ -102,33 +140,33 @@ pub fn use_router_service<R: Routable>(cx: Context) -> Option<&Rc<RouterService<
|
||||
#[derive(Props)]
|
||||
pub struct LinkProps<R: Routable> {
|
||||
to: R,
|
||||
|
||||
/// The url that gets pushed to the history stack
|
||||
///
|
||||
/// You can either put it your own inline method or just autoderive the route using `derive(Routable)`
|
||||
///
|
||||
/// ```rust
|
||||
///
|
||||
/// Link { to: Route::Home, href: |_| "home".to_string() }
|
||||
///
|
||||
/// // or
|
||||
///
|
||||
/// Link { to: Route::Home, href: Route::as_url }
|
||||
///
|
||||
/// ```
|
||||
href: fn(&R) -> String,
|
||||
|
||||
#[builder(default)]
|
||||
children: Element,
|
||||
}
|
||||
|
||||
pub fn Link<'a, R: Routable>(cx: Context, props: &LinkProps<R>) -> Element {
|
||||
pub fn Link<R: Routable>(cx: Context, props: &LinkProps<R>) -> Element {
|
||||
let service = use_router_service::<R>(cx)?;
|
||||
cx.render(rsx! {
|
||||
a {
|
||||
href: format_args!("{}", props.to.to_path()),
|
||||
href: format_args!("{}", (props.href)(&props.to)),
|
||||
onclick: move |_| service.push_route(props.to.clone()),
|
||||
{&props.children},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub trait Routable: Sized + Clone + 'static {
|
||||
/// Converts path to an instance of the routes enum.
|
||||
fn from_path(path: &str, params: &HashMap<&str, &str>) -> Option<Self>;
|
||||
|
||||
/// Converts the route to a string that can passed to the history API.
|
||||
fn to_path(&self) -> String;
|
||||
|
||||
/// Lists all the available routes
|
||||
fn routes() -> Vec<&'static str>;
|
||||
|
||||
/// The route to redirect to on 404
|
||||
fn not_found_route() -> Option<Self>;
|
||||
|
||||
/// Match a route based on the path
|
||||
fn recognize(pathname: &str) -> Option<Self>;
|
||||
}
|
||||
@@ -1,31 +1,6 @@
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::window;
|
||||
|
||||
pub fn fetch_base_url() -> Option<String> {
|
||||
match window()
|
||||
.unwrap()
|
||||
.document()
|
||||
.unwrap()
|
||||
.query_selector("base[href]")
|
||||
{
|
||||
Ok(Some(base)) => {
|
||||
let base = JsCast::unchecked_into::<web_sys::HtmlBaseElement>(base).href();
|
||||
|
||||
let url = web_sys::Url::new(&base).unwrap();
|
||||
let base = url.pathname();
|
||||
|
||||
let base = if base != "/" {
|
||||
strip_slash_suffix(&base)
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(base.to_string())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn strip_slash_suffix(path: &str) -> &str {
|
||||
path.strip_suffix('/').unwrap_or(path)
|
||||
}
|
||||
@@ -5,7 +5,7 @@ Render a Dioxus VirtualDOM to a string.
|
||||
|
||||
```rust
|
||||
// Our app:
|
||||
const App: FC<()> = |(cx, props)| rsx!(cx, div {"hello world!"});
|
||||
const App: FC<()> = |cx, props| rsx!(cx, div {"hello world!"});
|
||||
|
||||
// Build the virtualdom from our app
|
||||
let mut vdom = VirtualDOM::new(App);
|
||||
|
||||
@@ -110,7 +110,7 @@ pub fn render_vdom_scope(vdom: &VirtualDom, scope: ScopeId) -> Option<String> {
|
||||
///
|
||||
/// ## Example
|
||||
/// ```ignore
|
||||
/// static App: FC<()> = |(cx, props)|cx.render(rsx!(div { "hello world" }));
|
||||
/// static App: FC<()> = |cx, props|cx.render(rsx!(div { "hello world" }));
|
||||
/// let mut vdom = VirtualDom::new(App);
|
||||
/// vdom.rebuild();
|
||||
///
|
||||
@@ -149,7 +149,7 @@ impl<'a> TextRenderer<'a, '_> {
|
||||
}
|
||||
write!(f, "{}", text.text)?
|
||||
}
|
||||
VNode::Anchor(_anchor) => {
|
||||
VNode::Placeholder(_anchor) => {
|
||||
//
|
||||
if self.cfg.indent {
|
||||
for _ in 0..il {
|
||||
@@ -158,7 +158,7 @@ impl<'a> TextRenderer<'a, '_> {
|
||||
}
|
||||
write!(f, "<!-- -->")?;
|
||||
}
|
||||
VNode::Linked(link) => {
|
||||
VNode::Portal(link) => {
|
||||
todo!();
|
||||
}
|
||||
VNode::Element(el) => {
|
||||
@@ -252,9 +252,6 @@ impl<'a> TextRenderer<'a, '_> {
|
||||
} else {
|
||||
}
|
||||
}
|
||||
VNode::Suspended { .. } => {
|
||||
// we can't do anything with suspended nodes
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+16
-2
@@ -11,10 +11,17 @@ license = "MIT/Apache-2.0"
|
||||
dioxus-core = { path = "../core", version = "0.1.2" }
|
||||
dioxus-html = { path = "../html" }
|
||||
js-sys = "0.3"
|
||||
# wasm-bindgen-shared = { path = "../../../Tinkering/wasm-bindgen/crates/shared" }
|
||||
# wasm-bindgen-macro-support = { path = "../../../Tinkering/wasm-bindgen/crates/macro-support" }
|
||||
# wasm-bindgen = { features = [
|
||||
|
||||
|
||||
wasm-bindgen = { version = "0.2.78", features = ["enable-interning"] }
|
||||
# wasm-bindgen = { version = "0.2.78", features = ["enable-interning"] }
|
||||
# wasm-bindgen = { version = "0.2.78", features = ["enable-interning"] }
|
||||
lazy_static = "1.4.0"
|
||||
wasm-bindgen-futures = "0.4.20"
|
||||
log = "0.4.14"
|
||||
log = { version = "0.4.14", features = ["release_max_level_off"] }
|
||||
fxhash = "0.2.1"
|
||||
wasm-logger = "0.2.0"
|
||||
console_error_panic_hook = "0.1.6"
|
||||
@@ -24,6 +31,7 @@ async-channel = "1.6.1"
|
||||
anyhow = "1.0"
|
||||
gloo-timers = { version = "0.2.1", features = ["futures"] }
|
||||
futures-util = "0.3.15"
|
||||
smallstr = "0.2.0"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3.51"
|
||||
@@ -65,6 +73,7 @@ features = [
|
||||
"IdleDeadline",
|
||||
]
|
||||
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
@@ -76,7 +85,12 @@ serde = { version = "1.0.126", features = ["derive"] }
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
dioxus-hooks = { path = "../hooks" }
|
||||
dioxus-core-macro = { path = "../core-macro" }
|
||||
# rand = { version="0.8.4", features=["small_rng"] }
|
||||
rand = { version = "0.8.4", features = ["small_rng"] }
|
||||
|
||||
[dev-dependencies.getrandom]
|
||||
version = "0.2"
|
||||
features = ["js"]
|
||||
|
||||
# surf = { version = "2.3.1", default-features = false, features = [
|
||||
# "wasm-client",
|
||||
# ] }
|
||||
@@ -0,0 +1,243 @@
|
||||
use std::cell::Cell;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_core as dioxus;
|
||||
use dioxus_core_macro::*;
|
||||
use dioxus_hooks::{use_ref, use_state};
|
||||
use dioxus_html as dioxus_elements;
|
||||
use dioxus_web;
|
||||
use gloo_timers::future::TimeoutFuture;
|
||||
use rand::prelude::*;
|
||||
|
||||
fn main() {
|
||||
console_error_panic_hook::set_once();
|
||||
if cfg!(debug_assertions) {
|
||||
wasm_logger::init(wasm_logger::Config::new(log::Level::Debug));
|
||||
log::debug!("hello world");
|
||||
}
|
||||
|
||||
for a in ADJECTIVES {
|
||||
wasm_bindgen::intern(*a);
|
||||
}
|
||||
for a in COLOURS {
|
||||
wasm_bindgen::intern(*a);
|
||||
}
|
||||
for a in NOUNS {
|
||||
wasm_bindgen::intern(*a);
|
||||
}
|
||||
for a in [
|
||||
"container",
|
||||
"jumbotron",
|
||||
"row",
|
||||
"Dioxus",
|
||||
"col-md-6",
|
||||
"col-md-1",
|
||||
"Create 1,000 rows",
|
||||
"run",
|
||||
"Create 10,000 rows",
|
||||
"runlots",
|
||||
"Append 1,000 rows",
|
||||
"add",
|
||||
"Update every 10th row",
|
||||
"update",
|
||||
"Clear",
|
||||
"clear",
|
||||
"Swap rows",
|
||||
"swaprows",
|
||||
"preloadicon glyphicon glyphicon-remove", //
|
||||
"aria-hidden",
|
||||
"onclick",
|
||||
"true",
|
||||
"false",
|
||||
"danger",
|
||||
"type",
|
||||
"id",
|
||||
"class",
|
||||
"glyphicon glyphicon-remove remove",
|
||||
"dioxus-id",
|
||||
"dioxus-event-click",
|
||||
"dioxus",
|
||||
"click",
|
||||
"1.10",
|
||||
"lbl",
|
||||
"remove",
|
||||
"dioxus-event",
|
||||
"col-sm-6 smallpad",
|
||||
"btn btn-primary btn-block",
|
||||
"",
|
||||
" ",
|
||||
] {
|
||||
wasm_bindgen::intern(a);
|
||||
}
|
||||
for x in 0..100_000 {
|
||||
wasm_bindgen::intern(&x.to_string());
|
||||
}
|
||||
|
||||
dioxus_web::launch(App, |c| c.rootname("main"));
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Copy)]
|
||||
struct Label {
|
||||
key: usize,
|
||||
labels: [&'static str; 3],
|
||||
}
|
||||
|
||||
static mut Counter: Cell<usize> = Cell::new(1);
|
||||
|
||||
impl Label {
|
||||
fn new_list(num: usize) -> Vec<Self> {
|
||||
let mut rng = SmallRng::from_entropy();
|
||||
let mut labels = Vec::with_capacity(num);
|
||||
|
||||
let offset = unsafe { Counter.get() };
|
||||
unsafe { Counter.set(offset + num) };
|
||||
|
||||
for k in offset..(offset + num) {
|
||||
labels.push(Label {
|
||||
key: k,
|
||||
labels: [
|
||||
ADJECTIVES.choose(&mut rng).unwrap(),
|
||||
COLOURS.choose(&mut rng).unwrap(),
|
||||
NOUNS.choose(&mut rng).unwrap(),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
labels
|
||||
}
|
||||
}
|
||||
|
||||
static App: FC<()> = |cx, _props| {
|
||||
let mut items = use_ref(cx, || vec![]);
|
||||
let mut selected = use_state(cx, || None);
|
||||
|
||||
cx.render(rsx! {
|
||||
div { class: "container"
|
||||
div { class: "jumbotron"
|
||||
div { class: "row"
|
||||
div { class: "col-md-6", h1 { "Dioxus" } }
|
||||
div { class: "col-md-6"
|
||||
div { class: "row"
|
||||
ActionButton { name: "Create 1,000 rows", id: "run",
|
||||
onclick: move || items.set(Label::new_list(1_000)),
|
||||
}
|
||||
ActionButton { name: "Create 10,000 rows", id: "runlots",
|
||||
onclick: move || items.set(Label::new_list(10_000)),
|
||||
}
|
||||
ActionButton { name: "Append 1,000 rows", id: "add",
|
||||
onclick: move || items.write().extend(Label::new_list(1_000)),
|
||||
}
|
||||
ActionButton { name: "Update every 10th row", id: "update",
|
||||
onclick: move || items.write().iter_mut().step_by(10).for_each(|item| item.labels[2] = "!!!"),
|
||||
}
|
||||
ActionButton { name: "Clear", id: "clear",
|
||||
onclick: move || items.write().clear(),
|
||||
}
|
||||
ActionButton { name: "Swap Rows", id: "swaprows",
|
||||
onclick: move || items.write().swap(0, 998),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
table { class: "table table-hover table-striped test-data"
|
||||
tbody { id: "tbody"
|
||||
{items.read().iter().enumerate().map(|(id, item)| {
|
||||
let [adj, col, noun] = item.labels;
|
||||
let is_in_danger = if (*selected).map(|s| s == id).unwrap_or(false) {"danger"} else {""};
|
||||
rsx!(tr {
|
||||
class: "{is_in_danger}",
|
||||
key: "{id}",
|
||||
td { class:"col-md-1" }
|
||||
td { class:"col-md-1", "{item.key}" }
|
||||
td { class:"col-md-1", onclick: move |_| selected.set(Some(id)),
|
||||
a { class: "lbl", "{adj} {col} {noun}" }
|
||||
}
|
||||
td { class: "col-md-1"
|
||||
a { class: "remove", onclick: move |_| { items.write().remove(id); },
|
||||
span { class: "glyphicon glyphicon-remove remove" aria_hidden: "true" }
|
||||
}
|
||||
}
|
||||
td { class: "col-md-6" }
|
||||
})
|
||||
})}
|
||||
}
|
||||
}
|
||||
span { class: "preloadicon glyphicon glyphicon-remove" aria_hidden: "true" }
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
#[derive(Props)]
|
||||
struct ActionButtonProps<'a> {
|
||||
name: &'static str,
|
||||
id: &'static str,
|
||||
onclick: &'a dyn Fn(),
|
||||
}
|
||||
|
||||
fn ActionButton(cx: Context, props: &ActionButtonProps) -> Element {
|
||||
rsx!(cx, div { class: "col-sm-6 smallpad"
|
||||
button { class:"btn btn-primary btn-block", r#type: "button", id: "{props.id}", onclick: move |_| (props.onclick)(),
|
||||
"{props.name}"
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static ADJECTIVES: &[&str] = &[
|
||||
"pretty",
|
||||
"large",
|
||||
"big",
|
||||
"small",
|
||||
"tall",
|
||||
"short",
|
||||
"long",
|
||||
"handsome",
|
||||
"plain",
|
||||
"quaint",
|
||||
"clean",
|
||||
"elegant",
|
||||
"easy",
|
||||
"angry",
|
||||
"crazy",
|
||||
"helpful",
|
||||
"mushy",
|
||||
"odd",
|
||||
"unsightly",
|
||||
"adorable",
|
||||
"important",
|
||||
"inexpensive",
|
||||
"cheap",
|
||||
"expensive",
|
||||
"fancy",
|
||||
];
|
||||
|
||||
static COLOURS: &[&str] = &[
|
||||
"red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black",
|
||||
"orange",
|
||||
];
|
||||
|
||||
static NOUNS: &[&str] = &[
|
||||
"table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger",
|
||||
"pizza", "mouse", "keyboard",
|
||||
];
|
||||
|
||||
// #[derive(PartialEq, Props)]
|
||||
// struct RowProps<'a> {
|
||||
// row_id: usize,
|
||||
// label: &'a Label,
|
||||
// }
|
||||
|
||||
// fn Row(cx: Context, props: &RowProps) -> Element {
|
||||
// rsx!(cx, tr {
|
||||
// td { class:"col-md-1", "{props.row_id}" }
|
||||
// td { class:"col-md-1", onclick: move |_| { /* run onselect */ }
|
||||
// a { class: "lbl", {props.label.labels} }
|
||||
// }
|
||||
// td { class: "col-md-1"
|
||||
// a { class: "remove", onclick: move |_| {/* remove */}
|
||||
// span { class: "glyphicon glyphicon-remove remove" aria_hidden: "true" }
|
||||
// }
|
||||
// }
|
||||
// td { class: "col-md-6" }
|
||||
// })
|
||||
// }
|
||||
@@ -0,0 +1,46 @@
|
||||
//! Example: README.md showcase
|
||||
//!
|
||||
//! The example from the README.md.
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_core as dioxus;
|
||||
use dioxus_core_macro::*;
|
||||
use dioxus_hooks::use_state;
|
||||
use dioxus_html as dioxus_elements;
|
||||
use dioxus_web;
|
||||
use gloo_timers::future::TimeoutFuture;
|
||||
|
||||
fn main() {
|
||||
wasm_logger::init(wasm_logger::Config::new(log::Level::Debug));
|
||||
dioxus_web::launch(App, |c| c);
|
||||
}
|
||||
|
||||
static App: FC<()> = |cx, props| {
|
||||
let show = use_state(cx, || true);
|
||||
|
||||
let inner = match *show {
|
||||
true => {
|
||||
rsx!( div {
|
||||
"hello world"
|
||||
})
|
||||
}
|
||||
false => {
|
||||
rsx!( div {
|
||||
// h1 {
|
||||
"bello world"
|
||||
// }
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
rsx!(cx, div {
|
||||
button {
|
||||
"toggle"
|
||||
onclick: move |_| {
|
||||
let cur = *show;
|
||||
show.set(!cur);
|
||||
}
|
||||
}
|
||||
{inner}
|
||||
})
|
||||
};
|
||||
@@ -7,6 +7,11 @@
|
||||
/// Eventually we might want to procedurally generate these strings for common words, phrases, and values.
|
||||
pub(crate) fn intern_cached_strings() {
|
||||
let cached_words = [
|
||||
// Important tags to dioxus
|
||||
"dioxus-id",
|
||||
"dioxus",
|
||||
"dioxus-event-click", // todo: more events
|
||||
"click",
|
||||
// All the HTML Tags
|
||||
"a",
|
||||
"abbr",
|
||||
|
||||
@@ -17,7 +17,7 @@ impl Default for WebConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
hydrate: false,
|
||||
rootname: "dioxusroot".to_string(),
|
||||
rootname: "main".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+45
-23
@@ -213,7 +213,7 @@ impl WebsysDom {
|
||||
|
||||
fn create_placeholder(&mut self, id: u64) {
|
||||
self.create_element("pre", None, id);
|
||||
// self.set_attribute("hidden", "", None);
|
||||
self.set_attribute("hidden", "", None, id);
|
||||
}
|
||||
|
||||
fn create_text_node(&mut self, text: &str, id: u64) {
|
||||
@@ -246,8 +246,15 @@ impl WebsysDom {
|
||||
.unwrap(),
|
||||
};
|
||||
|
||||
use smallstr;
|
||||
use smallstr::SmallString;
|
||||
use std::fmt::Write;
|
||||
|
||||
let mut s: SmallString<[u8; 8]> = smallstr::SmallString::new();
|
||||
write!(s, "{}", id).unwrap();
|
||||
|
||||
let el2 = el.dyn_ref::<Element>().unwrap();
|
||||
el2.set_attribute("dioxus-id", &format!("{}", id)).unwrap();
|
||||
el2.set_attribute("dioxus-id", s.as_str()).unwrap();
|
||||
|
||||
self.stack.push(el.clone());
|
||||
self.nodes[(id as usize)] = Some(el);
|
||||
@@ -263,18 +270,21 @@ impl WebsysDom {
|
||||
|
||||
let el = self.stack.top();
|
||||
|
||||
let el = el
|
||||
.dyn_ref::<Element>()
|
||||
.expect(&format!("not an element: {:?}", el));
|
||||
let el = el.dyn_ref::<Element>().unwrap();
|
||||
// let el = el.dyn_ref::<Element>().unwrap();
|
||||
// .expect(&format!("not an element: {:?}", el));
|
||||
|
||||
// let scope_id = scope.data().as_ffi();
|
||||
let scope_id = scope.0 as u64;
|
||||
// let scope_id = scope.0 as u64;
|
||||
// "dioxus-event-click",
|
||||
// "1.10"
|
||||
// &format!("", scope_id, real_id),
|
||||
// &format!("dioxus-event-{}", event),
|
||||
// &format!("{}.{}", scope_id, real_id),
|
||||
// &format!("dioxus-event-{}", event),
|
||||
// &format!("{}.{}", scope_id, real_id),
|
||||
|
||||
el.set_attribute(
|
||||
&format!("dioxus-event-{}", event),
|
||||
&format!("{}.{}", scope_id, real_id),
|
||||
)
|
||||
.unwrap();
|
||||
el.set_attribute("dioxus-event", event).unwrap();
|
||||
|
||||
// el.set_attribute(&format!("dioxus-event"), &format!("{}", event))
|
||||
// .unwrap();
|
||||
@@ -488,6 +498,8 @@ unsafe impl Sync for DioxusWebsysEvent {}
|
||||
fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc<dyn Any + Send + Sync> {
|
||||
use dioxus_html::on::*;
|
||||
use dioxus_html::KeyCode;
|
||||
// event.prevent_default();
|
||||
|
||||
// use dioxus_core::events::on::*;
|
||||
match event.type_().as_str() {
|
||||
"copy" | "cut" | "paste" => Arc::new(ClipboardEvent {}),
|
||||
@@ -682,30 +694,40 @@ fn decode_trigger(event: &web_sys::Event) -> anyhow::Result<UserEvent> {
|
||||
|
||||
use anyhow::Context;
|
||||
|
||||
let element_id = target
|
||||
.get_attribute("dioxus-id")
|
||||
.context("Could not find element id on event target")?
|
||||
.parse()?;
|
||||
|
||||
// The error handling here is not very descriptive and needs to be replaced with a zero-cost error system
|
||||
let val: String = target
|
||||
.get_attribute(&format!("dioxus-event-{}", typ))
|
||||
.get_attribute("dioxus-event")
|
||||
.context(format!("wrong format - received {:#?}", typ))?;
|
||||
// .get_attribute(&format!("dioxus-event-{}", typ))
|
||||
// .context(format!("wrong format - received {:#?}", typ))?;
|
||||
|
||||
let mut fields = val.splitn(3, ".");
|
||||
|
||||
let gi_id = fields
|
||||
.next()
|
||||
.and_then(|f| f.parse::<u64>().ok())
|
||||
.context("failed to parse gi id")?;
|
||||
// let gi_id = fields
|
||||
// .next()
|
||||
// .and_then(|f| f.parse::<u64>().ok())
|
||||
// .context("failed to parse gi id")?;
|
||||
|
||||
let real_id = fields
|
||||
.next()
|
||||
.and_then(|raw_id| raw_id.parse::<u64>().ok())
|
||||
.context("failed to parse real id")?;
|
||||
// let real_id = fields
|
||||
// .next()
|
||||
// .and_then(|raw_id| raw_id.parse::<u64>().ok())
|
||||
// .context("failed to parse real id")?;
|
||||
|
||||
let triggered_scope = gi_id;
|
||||
// let triggered_scope = gi_id;
|
||||
|
||||
Ok(UserEvent {
|
||||
name: event_name_from_typ(&typ),
|
||||
data: virtual_event_from_websys_event(event.clone()),
|
||||
element: Some(ElementId(real_id as usize)),
|
||||
scope_id: Some(ScopeId(triggered_scope as usize)),
|
||||
element: Some(ElementId(element_id)),
|
||||
scope_id: None,
|
||||
// scope_id: Some(ScopeId(triggered_scope as usize)),
|
||||
// element: Some(ElementId(real_id as usize)),
|
||||
// scope_id: Some(ScopeId(triggered_scope as usize)),
|
||||
priority: dioxus_core::EventPriority::Medium,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ mod ric_raf;
|
||||
/// dioxus_web::launch(App, |c| c);
|
||||
/// }
|
||||
///
|
||||
/// static App: FC<()> = |(cx, props)| {
|
||||
/// static App: FC<()> = |cx, props| {
|
||||
/// rsx!(cx, div {"hello world"})
|
||||
/// }
|
||||
/// ```
|
||||
@@ -109,7 +109,7 @@ pub fn launch(root_component: FC<()>, configuration: impl FnOnce(WebConfig) -> W
|
||||
/// name: String
|
||||
/// }
|
||||
///
|
||||
/// static App: FC<RootProps> = |(cx, props)| {
|
||||
/// static App: FC<RootProps> = |cx, props| {
|
||||
/// rsx!(cx, div {"hello {props.name}"})
|
||||
/// }
|
||||
/// ```
|
||||
@@ -155,7 +155,7 @@ pub async fn run_with_props<T: 'static + Send>(root: FC<T>, root_props: T, cfg:
|
||||
// hydrating is simply running the dom for a single render. If the page is already written, then the corresponding
|
||||
// ElementIds should already line up because the web_sys dom has already loaded elements with the DioxusID into memory
|
||||
if !should_hydrate {
|
||||
log::info!("Applying rebuild edits..., {:?}", mutations);
|
||||
// log::info!("Applying rebuild edits..., {:?}", mutations);
|
||||
websys_dom.process_edits(&mut mutations.edits);
|
||||
}
|
||||
|
||||
@@ -166,17 +166,20 @@ pub async fn run_with_props<T: 'static + Send>(root: FC<T>, root_props: T, cfg:
|
||||
// if there is work then this future resolves immediately.
|
||||
dom.wait_for_work().await;
|
||||
|
||||
// wait for the mainthread to schedule us in
|
||||
let mut deadline = work_loop.wait_for_idle_time().await;
|
||||
// // wait for the mainthread to schedule us in
|
||||
// let mut deadline = work_loop.wait_for_idle_time().await;
|
||||
|
||||
// run the virtualdom work phase until the frame deadline is reached
|
||||
let mutations = dom.work_with_deadline(|| (&mut deadline).now_or_never().is_some());
|
||||
let mutations = dom.work_with_deadline(|| false);
|
||||
// // run the virtualdom work phase until the frame deadline is reached
|
||||
// let mutations = dom.work_with_deadline(|| (&mut deadline).now_or_never().is_some());
|
||||
|
||||
// wait for the animation frame to fire so we can apply our changes
|
||||
work_loop.wait_for_raf().await;
|
||||
|
||||
for mut edit in mutations {
|
||||
// actually apply our changes during the animation frame
|
||||
// log::info!("Applying change edits..., {:?}", edit);
|
||||
websys_dom.process_edits(&mut edit.edits);
|
||||
}
|
||||
}
|
||||
|
||||
+97
-137
@@ -4,166 +4,124 @@
|
||||
//! <strong>A concurrent, functional, virtual DOM for Rust</strong>
|
||||
//! </p>
|
||||
//! </div>
|
||||
//! Dioxus: a concurrent, functional, reactive virtual dom for any renderer in Rust.
|
||||
//!
|
||||
//! This crate aims to maintain a hook-based, renderer-agnostic framework for cross-platform UI development.
|
||||
//! # Resources
|
||||
//!
|
||||
//! ## Overview and Goals
|
||||
//! Dioxus' ultimate goal is to save you from writing new code when bringing your application to new platforms. We forsee
|
||||
//! a future where WebApps, Mobile Apps, Desktop Apps, and even AR apps can be written in the same language, ecosystem,
|
||||
//! and leverage the same platform-agnostic libraries.
|
||||
//! This overview is provides a brief introduction to Dioxus. For a more in-depth guide, make sure to check out:
|
||||
//! - [Getting Started](https://dioxuslabs.com/getting-started)
|
||||
//! - [Book](https://dioxuslabs.com/book)
|
||||
//! - [Reference](https://dioxuslabs.com/refernce-guide)
|
||||
|
||||
//!
|
||||
//! In this aim we chose to use a variety of techniques:
|
||||
//! - We use a VirtualDOM to abstract the true renderer from application logic.
|
||||
//! - We use functions as components to limit the API churn for greater stability.
|
||||
//! - We use hooks as state to allow reusable logic across the whole ecosystem.
|
||||
//! - We support an extensible and compile-time safe DSL for building interfaces.
|
||||
//! # Overview and Goals
|
||||
//!
|
||||
//! Our guiding stars (in order of priority):
|
||||
//! - Ergonomics
|
||||
//! - Reusability
|
||||
//! - Speed and memory efficiency
|
||||
//! - Safety
|
||||
//! Dioxus makes it easy to quickly build complex user interfaces with Rust. Any Dioxus app can run in the web browser,
|
||||
//! as a desktop app, as a mobile app, or anywhere else provided you build the right renderer.
|
||||
//!
|
||||
//! ## Components
|
||||
//! The base unit of Dioxus is the `component`. Components can be easily created from just a function - no traits or
|
||||
//! proc macros required:
|
||||
//! Dioxus is heavily inspired by React, supporting many of the same concepts:
|
||||
//!
|
||||
//! ```
|
||||
//! - Hooks for state
|
||||
//! - VirtualDom & diffing
|
||||
//! - Concurrency & asynchronous rendering
|
||||
//! - JSX-like templating syntax
|
||||
//!
|
||||
//! If you know React, then you know Dioxus.
|
||||
//!
|
||||
//! Dioxus is *substantially* faster than many of the other Rust UI libraries (Yew/Percy) and is *significantly* faster
|
||||
//! than React, competitve with InfernoJS and frameworks like Svelte/SolidJS.
|
||||
//!
|
||||
//! ## Brief Overview
|
||||
//!
|
||||
//! All Dioxus apps are built by composing functions that take in a `Scope` and `Properties` and return an `Element`. A `Scope` holds
|
||||
//! relevant state data for the the currently-rendered component.
|
||||
//!
|
||||
//! ```rust
|
||||
//! use dioxus::prelude::*;
|
||||
//!
|
||||
//! fn Example(cx: Context<()>) -> DomTree {
|
||||
//! html! { <div> "Hello, world!" </div> }
|
||||
//! fn main() {
|
||||
//! dioxus::desktop::launch(App);
|
||||
//! }
|
||||
//! ```
|
||||
//! Components need to take a "Context" parameter which is generic over some properties. This defines how the component can be used
|
||||
//! and what properties can be used to specify it in the VNode output. Components without properties may be generic over
|
||||
//! `()`, and components with properties must declare their properties as a struct:
|
||||
//!
|
||||
//! ```
|
||||
//! #[derive(Props)]
|
||||
//! struct Props { name: String }
|
||||
//! fn App(cx: Scope, props: &()) -> Element {
|
||||
//! let mut count = use_state(cx, || 0);
|
||||
//!
|
||||
//! fn Example(cx: Context<Props>) -> DomTree {
|
||||
//! html! { <div> "Hello {cx.props.name}!" </div> }
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! Props that are valid for the `'pub static` lifetime automatically get memoized by Diouxs. This means the component won't
|
||||
//! re-render if its Props didn't change. However, Props that borrow data from their parent cannot be safely memoized, and
|
||||
//! will always re-render if their parent changes. To borrow data from a parent, your component needs to add explicit lifetimes,
|
||||
//! otherwise Rust will get confused about whether data is borrowed from either Props or Context. Since Dioxus manages
|
||||
//! these lifetimes internally, Context and your Props must share the same lifetime:
|
||||
//!
|
||||
//! ```
|
||||
//! #[derive(Props)]
|
||||
//! struct Props<'a> { name: &'a str }
|
||||
//!
|
||||
//! fn Example<'a>(cx: Context<'a, Props<'a>>) -> DomTree {
|
||||
//! html! { <div> "Hello {cx.props.name}!" </div> }
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
//! The lifetimes might look a little messy, but are crucially important for Dioxus's efficiency and overall ergonimics.
|
||||
//! Components can also be crafted as pub static closures, enabling type inference without all the type signature noise. However,
|
||||
//! closure-style components cannot work with borrowed data due to limitations in Rust's lifetime system.
|
||||
//!
|
||||
//! To use custom properties for components, you'll need to derive the `Props` trait for your properties. This trait
|
||||
//! exposes a compile-time correct builder pattern (similar to typed-builder) that can be used in the `rsx!` and `html!`
|
||||
//! macros to build components. Component props may have default fields notated by the `Default` attribute:
|
||||
//!
|
||||
//! ```
|
||||
//! #[derive(Props)]
|
||||
//! struct Props {
|
||||
//! name: String
|
||||
//!
|
||||
//! #[props(default = false)]
|
||||
//! checked: bool,
|
||||
//!
|
||||
//! #[props(default, setter(strip_option, into))]
|
||||
//! title: Option<String>
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! These flags roughly follow that of typed-builder, though tweaked to support the `Props` usecase.
|
||||
//!
|
||||
//! ## Hooks and State
|
||||
//! Dioxus uses hooks for state management. Hooks are a form of state persisted between calls of the function component.
|
||||
//!
|
||||
//! ```
|
||||
//! pub pub static Example: FC<()> = |(cx, props)|{
|
||||
//! let (val, set_val) = use_state(cx, || 0);
|
||||
//! cx.render(rsx!(
|
||||
//! button { onclick: move |_| set_val(val + 1) }
|
||||
//! div { "Count: {count}" }
|
||||
//! button { onclick: move |_| count += 1, "Increment" }
|
||||
//! button { onclick: move |_| count -= 1, "Decrement" }
|
||||
//! ))
|
||||
//! }
|
||||
//! ````
|
||||
//!
|
||||
//! Instead of using a single struct to represent a component and its state, hooks use the "use_hook" building block
|
||||
//! which allows the persistence of data between function component renders. This primitive is exposed directly through
|
||||
//! the `Context` item:
|
||||
//! ```
|
||||
//! fn my_hook<'a>(cx: &impl Scoped<'a>) -> &'a String {
|
||||
//! cx.use_hook(
|
||||
//! // Initializer stores a value
|
||||
//! |hook_idx| String::new("stored_data"),
|
||||
//!
|
||||
//! // Runner returns the hook value every time the component is rendered
|
||||
//! |hook| &*hook,
|
||||
//! ## Components
|
||||
//!
|
||||
//! // Cleanup runs after the component is unmounted
|
||||
//! |hook| log::debug!("cleaning up hook with value {:#?}", hook)
|
||||
//! )
|
||||
//! We can compose these function components to build a complex app. Each new component we design must take some Properties.
|
||||
//! For components with no explicit properties, we can use the `()` type. In Dioxus, all properties are memoized by default!
|
||||
//!
|
||||
//! ```rust
|
||||
//! fn App(cx: Scope, props &()) -> Element {
|
||||
//! cx.render(rsx!(
|
||||
//! Header {
|
||||
//! title: "My App",
|
||||
//! color: "red",
|
||||
//! }
|
||||
//! ))
|
||||
//! }
|
||||
//! ```
|
||||
//! Under the hood, hooks store their data in a series of "memory cells". The first render defines the layout of these
|
||||
//! memory cells, and on each subsequent render, each `use_hook` call accesses its corresponding memory cell. If a hook
|
||||
//! accesses the wrong memory cell, `use_hook` will panic, and your app will crash. You can always use `try_use_hook` but
|
||||
//! these types of errors can be easily mitigated by following the rules of hooks:
|
||||
//!
|
||||
//! - Don’t call Hooks inside loops, conditions, or nested functions
|
||||
//! - Don't call hooks in changing order between renders
|
||||
//!
|
||||
//! Hooks provide a very powerful way to reuse stateful logic between components, simplify large complex components,
|
||||
//! and adopt more clear context subscription patterns to make components easier to read. The mechanics of hooks in Dioxus
|
||||
//! shares a great amount of similarity with React's hooks and there are many guides to hooks in React online.
|
||||
//!
|
||||
//! ## Supported Renderers
|
||||
//! Instead of being tightly coupled to a platform, browser, or toolkit, Dioxus implements a VirtualDOM object which
|
||||
//! can be consumed to draw the UI. The Dioxus VDOM is reactive and easily consumable by 3rd-party renderers via
|
||||
//! the `RealDom` trait. See [Implementing a Renderer](docs/8-custom-renderer.md), the `StringRenderer`, and `WebSys` render implementations for a template
|
||||
//! on how to implement your own custom renderer. We provide 1st-class support for these renderers:
|
||||
//!
|
||||
//! - dioxus-desktop (via WebView)
|
||||
//! - dioxus-web (via WebSys)
|
||||
//! - dioxus-ssr (via StringRenderer)
|
||||
//! - dioxus-liveview (SSR + WebSys)
|
||||
//!
|
||||
//! In the main `dioxus` crate, these are all accessible through configuration flags.
|
||||
//!
|
||||
//! ## Rendering to the Web
|
||||
//!
|
||||
//! Most dioxus apps will be initialized in roughly the same way. The `launch` method in `web` will immediately start a
|
||||
//! VirtualDOM and await it using `wasm_bindgen_futures`.
|
||||
//!
|
||||
//! An example app that starts a websys app and internally awaits works as follows:
|
||||
//!
|
||||
//! Our `Header` component takes in a `title` and a `color` property, which we delcare on an explicit `HeaderProps` struct.
|
||||
//! ```
|
||||
//! use dioxus::prelude::*;
|
||||
//! fn main() {
|
||||
//! dioxus::web::launch(Example);
|
||||
//! // The `Props` derive macro lets us add additional functionality to how props are interpreted.
|
||||
//! #[derive(Props, PartialEq)]
|
||||
//! struct HeaderProps {
|
||||
//! title: String,
|
||||
//! color: String,
|
||||
//! }
|
||||
//!
|
||||
//! pub pub static Example: FC<()> = |(cx, props)|{
|
||||
//! cx.render(rsx! {
|
||||
//! div { "Hello World!" }
|
||||
//! })
|
||||
//! };
|
||||
//! fn Header(cx: Scope, props: &HeaderProps) -> Element {
|
||||
//! cx.render(rsx!(
|
||||
//! div {
|
||||
//! background_color: "{props.color}"
|
||||
//! h1 { "{props.title}" }
|
||||
//! }
|
||||
//! ))
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! In reality, you'll want to integrate analytics, logging, crash-protection and more.
|
||||
//! ## Hooks
|
||||
//!
|
||||
//! While components are reusable forms of UI elements, hooks are reusable forms of logic. The details of hooks are
|
||||
//! somewhat complicated. In essence, hooks let us save state between renders of our components and reuse the accompanying
|
||||
//! logic across different apps.
|
||||
//!
|
||||
//! Hooks are simply composition of other hooks. To create our first hook we can create a simple function that takes in
|
||||
//! an Scope. We can then call `use_hook` on the `Scope` to get a mutable reference to the stored value.
|
||||
//!
|
||||
//! ```rust
|
||||
//! fn use_say_hello(cx: Scope) -> &mut String {
|
||||
//! cx.use_hook(|_| "Hello".to_string(), |hook| hook)
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! If you want to extend Dioxus with some new functionality, you'll probably want to implement a new hook.
|
||||
//!
|
||||
//!
|
||||
//! ## Features
|
||||
//!
|
||||
//! This overview doesn't cover everything. Make sure to check out the tutorial and reference guide on the official
|
||||
//! website for more details.
|
||||
//!
|
||||
//! Beyond this overview, Dioxus supports:
|
||||
//! - Server-side rendering
|
||||
//! - Concurrent rendering (with async support)
|
||||
//! - Web/Desktop/Mobile support
|
||||
//! - Pre-rendering and rehydration
|
||||
//! - Fragments, Portals, and Suspense
|
||||
//! - Inline-styles
|
||||
//! - Custom event handlers
|
||||
//! - Custom elements
|
||||
//! - Basic fine-grained reactivity (IE SolidJS/Svelte)
|
||||
//! - and more!
|
||||
|
||||
// Just a heads-up, the core functionality of dioxus rests in Dioxus-Core. This crate just wraps a bunch of utilities
|
||||
// together and exports their namespaces to something predicatble.
|
||||
@@ -188,10 +146,12 @@ pub use dioxus_desktop as desktop;
|
||||
#[cfg(feature = "router")]
|
||||
pub use dioxus_router as router;
|
||||
|
||||
pub mod debug {}
|
||||
pub mod events {
|
||||
#[cfg(feature = "html")]
|
||||
pub use dioxus_html::{on::*, KeyCode};
|
||||
}
|
||||
|
||||
pub mod prelude {
|
||||
//! A glob import that includes helper types like FC, rsx!, html!, and required traits
|
||||
pub use dioxus_core::prelude::*;
|
||||
pub use dioxus_core_macro::{format_args_f, rsx, Props, Routable};
|
||||
pub use dioxus_elements::{GlobalAttributes, SvgAttributes};
|
||||
|
||||
Reference in new issue
Block a user