Merge branch 'master' of github.com:DioxusLabs/dioxus into router-2
This commit is contained in:
commit
dd54c0acf7
53 files changed
+2195
-373
No files matched your search
+14
-1
@@ -1,4 +1,5 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_desktop::EvalResult;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
dioxus_desktop::launch(app);
|
dioxus_desktop::launch(app);
|
||||||
@@ -7,6 +8,15 @@ fn main() {
|
|||||||
fn app(cx: Scope) -> Element {
|
fn app(cx: Scope) -> Element {
|
||||||
let script = use_state(cx, String::new);
|
let script = use_state(cx, String::new);
|
||||||
let eval = dioxus_desktop::use_eval(cx);
|
let eval = dioxus_desktop::use_eval(cx);
|
||||||
|
let future: &UseRef<Option<EvalResult>> = use_ref(cx, || None);
|
||||||
|
if future.read().is_some() {
|
||||||
|
let future_clone = future.clone();
|
||||||
|
cx.spawn(async move {
|
||||||
|
if let Some(fut) = future_clone.with_mut(|o| o.take()) {
|
||||||
|
println!("{:?}", fut.await)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
div {
|
div {
|
||||||
@@ -16,7 +26,10 @@ fn app(cx: Scope) -> Element {
|
|||||||
oninput: move |e| script.set(e.value.clone()),
|
oninput: move |e| script.set(e.value.clone()),
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
onclick: move |_| eval(script.to_string()),
|
onclick: move |_| {
|
||||||
|
let fut = eval(script);
|
||||||
|
future.set(Some(fut));
|
||||||
|
},
|
||||||
"Execute"
|
"Execute"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
//! A tour of the rsx! macro
|
//! A tour of the rsx! macro
|
||||||
//! ------------------------
|
//! ------------------------
|
||||||
//!
|
//!
|
||||||
@@ -39,7 +38,7 @@
|
|||||||
//! - Accept a list of vnodes as children for a Fragment component
|
//! - Accept a list of vnodes as children for a Fragment component
|
||||||
//! - Allow keyed fragments in iterators
|
//! - Allow keyed fragments in iterators
|
||||||
//! - Allow top-level fragments
|
//! - Allow top-level fragments
|
||||||
//!
|
|
||||||
fn main() {
|
fn main() {
|
||||||
dioxus_desktop::launch(app);
|
dioxus_desktop::launch(app);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,12 @@ fn app(cx: Scope) -> Element {
|
|||||||
stroke: "blue",
|
stroke: "blue",
|
||||||
stroke_width: "5",
|
stroke_width: "5",
|
||||||
}
|
}
|
||||||
|
path {
|
||||||
|
d: "M9.00001 9C9 62 103.5 124 103.5 178",
|
||||||
|
stroke: "#3CC4DC",
|
||||||
|
"stroke-linecap": "square",
|
||||||
|
"stroke-width": "square",
|
||||||
|
}
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ proc-macro = true
|
|||||||
proc-macro2 = { version = "1.0" }
|
proc-macro2 = { version = "1.0" }
|
||||||
quote = "1.0"
|
quote = "1.0"
|
||||||
syn = { version = "1.0", features = ["full", "extra-traits"] }
|
syn = { version = "1.0", features = ["full", "extra-traits"] }
|
||||||
dioxus-rsx = { path = "../rsx" }
|
dioxus-rsx = { path = "../rsx", version = "0.0.1" }
|
||||||
|
|
||||||
# testing
|
# testing
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|||||||
@@ -26,11 +26,6 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The rsx! macro makes it easy for developers to write jsx-style markup in their components.
|
/// The rsx! macro makes it easy for developers to write jsx-style markup in their components.
|
||||||
///
|
|
||||||
/// ## Complete Reference Guide:
|
|
||||||
/// ```ignore
|
|
||||||
#[doc = include_str!("../../../examples/rsx_usage.rs")]
|
|
||||||
/// ```
|
|
||||||
#[proc_macro]
|
#[proc_macro]
|
||||||
pub fn rsx(s: TokenStream) -> TokenStream {
|
pub fn rsx(s: TokenStream) -> TokenStream {
|
||||||
match syn::parse::<rsx::CallBody>(s) {
|
match syn::parse::<rsx::CallBody>(s) {
|
||||||
@@ -42,11 +37,6 @@ pub fn rsx(s: TokenStream) -> TokenStream {
|
|||||||
/// The render! macro makes it easy for developers to write jsx-style markup in their components.
|
/// The render! macro makes it easy for developers to write jsx-style markup in their components.
|
||||||
///
|
///
|
||||||
/// The render macro automatically renders rsx - making it unhygenic.
|
/// The render macro automatically renders rsx - making it unhygenic.
|
||||||
///
|
|
||||||
/// ## Complete Reference Guide:
|
|
||||||
/// ```ignore
|
|
||||||
#[doc = include_str!("../../../examples/rsx_usage.rs")]
|
|
||||||
/// ```
|
|
||||||
#[proc_macro]
|
#[proc_macro]
|
||||||
pub fn render(s: TokenStream) -> TokenStream {
|
pub fn render(s: TokenStream) -> TokenStream {
|
||||||
match syn::parse::<rsx::CallBody>(s) {
|
match syn::parse::<rsx::CallBody>(s) {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
|
|||||||
bumpalo = { version = "3.6", features = ["collections", "boxed"] }
|
bumpalo = { version = "3.6", features = ["collections", "boxed"] }
|
||||||
|
|
||||||
# faster hashmaps
|
# faster hashmaps
|
||||||
fxhash = "0.2"
|
rustc-hash = "1.1.0"
|
||||||
|
|
||||||
# Used in diffing
|
# Used in diffing
|
||||||
longest-increasing-subsequence = "0.1.0"
|
longest-increasing-subsequence = "0.1.0"
|
||||||
@@ -35,6 +35,8 @@ indexmap = "1.7"
|
|||||||
serde = { version = "1", features = ["derive"], optional = true }
|
serde = { version = "1", features = ["derive"], optional = true }
|
||||||
anyhow = "1.0.66"
|
anyhow = "1.0.66"
|
||||||
|
|
||||||
|
smallbox = "0.8.1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { version = "*", features = ["full"] }
|
tokio = { version = "*", features = ["full"] }
|
||||||
dioxus = { path = "../dioxus" }
|
dioxus = { path = "../dioxus" }
|
||||||
|
|||||||
+11
-15
@@ -36,6 +36,9 @@ impl<'b> VirtualDom {
|
|||||||
|
|
||||||
let cur_scope = self.scope_stack.last().copied().unwrap();
|
let cur_scope = self.scope_stack.last().copied().unwrap();
|
||||||
|
|
||||||
|
// we know that this will generate at least one mutation per node
|
||||||
|
self.mutations.edits.reserve(template.template.roots.len());
|
||||||
|
|
||||||
let mut on_stack = 0;
|
let mut on_stack = 0;
|
||||||
for (root_idx, root) in template.template.roots.iter().enumerate() {
|
for (root_idx, root) in template.template.roots.iter().enumerate() {
|
||||||
// We might need to generate an ID for the root node
|
// We might need to generate an ID for the root node
|
||||||
@@ -208,19 +211,12 @@ impl<'b> VirtualDom {
|
|||||||
|
|
||||||
// If it's all dynamic nodes, then we don't need to register it
|
// If it's all dynamic nodes, then we don't need to register it
|
||||||
// Quickly run through and see if it's all just dynamic nodes
|
// Quickly run through and see if it's all just dynamic nodes
|
||||||
let dynamic_roots = template
|
if template.template.roots.iter().all(|root| {
|
||||||
.template
|
matches!(
|
||||||
.roots
|
root,
|
||||||
.iter()
|
TemplateNode::Dynamic { .. } | TemplateNode::DynamicText { .. }
|
||||||
.filter(|root| {
|
)
|
||||||
matches!(
|
}) {
|
||||||
root,
|
|
||||||
TemplateNode::Dynamic { .. } | TemplateNode::DynamicText { .. }
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.count();
|
|
||||||
|
|
||||||
if dynamic_roots == template.template.roots.len() {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,7 +287,7 @@ impl<'b> VirtualDom {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn create_fragment(&mut self, nodes: &'b [VNode<'b>]) -> usize {
|
pub(crate) fn create_fragment(&mut self, nodes: &'b [VNode<'b>]) -> usize {
|
||||||
nodes.iter().fold(0, |acc, child| acc + self.create(child))
|
nodes.iter().map(|child| self.create(child)).sum()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn create_component_node(
|
pub(super) fn create_component_node(
|
||||||
@@ -302,7 +298,7 @@ impl<'b> VirtualDom {
|
|||||||
) -> usize {
|
) -> usize {
|
||||||
let props = component
|
let props = component
|
||||||
.props
|
.props
|
||||||
.replace(None)
|
.take()
|
||||||
.expect("Props to always exist when a component is being created");
|
.expect("Props to always exist when a component is being created");
|
||||||
|
|
||||||
let unbounded_props = unsafe { std::mem::transmute(props) };
|
let unbounded_props = unsafe { std::mem::transmute(props) };
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use crate::{
|
|||||||
AttributeValue, TemplateNode,
|
AttributeValue, TemplateNode,
|
||||||
};
|
};
|
||||||
|
|
||||||
use fxhash::{FxHashMap, FxHashSet};
|
use rustc_hash::{FxHashMap, FxHashSet};
|
||||||
use DynamicNode::*;
|
use DynamicNode::*;
|
||||||
|
|
||||||
impl<'b> VirtualDom {
|
impl<'b> VirtualDom {
|
||||||
@@ -56,7 +56,9 @@ impl<'b> VirtualDom {
|
|||||||
fn diff_err_to_ok(&mut self, _e: &anyhow::Error, _l: &'b VNode<'b>) {}
|
fn diff_err_to_ok(&mut self, _e: &anyhow::Error, _l: &'b VNode<'b>) {}
|
||||||
|
|
||||||
fn diff_node(&mut self, left_template: &'b VNode<'b>, right_template: &'b VNode<'b>) {
|
fn diff_node(&mut self, left_template: &'b VNode<'b>, right_template: &'b VNode<'b>) {
|
||||||
if left_template.template.name != right_template.template.name {
|
if !std::ptr::eq(left_template.template.name, right_template.template.name)
|
||||||
|
&& left_template.template.name != right_template.template.name
|
||||||
|
{
|
||||||
return self.light_diff_templates(left_template, right_template);
|
return self.light_diff_templates(left_template, right_template);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -432,7 +434,7 @@ impl<'b> VirtualDom {
|
|||||||
// The stack is empty upon entry.
|
// The stack is empty upon entry.
|
||||||
fn diff_keyed_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) {
|
fn diff_keyed_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) {
|
||||||
if cfg!(debug_assertions) {
|
if cfg!(debug_assertions) {
|
||||||
let mut keys = fxhash::FxHashSet::default();
|
let mut keys = rustc_hash::FxHashSet::default();
|
||||||
let mut assert_unique_keys = |children: &'b [VNode<'b>]| {
|
let mut assert_unique_keys = |children: &'b [VNode<'b>]| {
|
||||||
keys.clear();
|
keys.clear();
|
||||||
for child in children {
|
for child in children {
|
||||||
|
|||||||
@@ -11,8 +11,9 @@
|
|||||||
//! The logic for this was borrowed from <https://docs.rs/stack_dst/0.6.1/stack_dst/>. Unfortunately, this crate does not
|
//! The logic for this was borrowed from <https://docs.rs/stack_dst/0.6.1/stack_dst/>. Unfortunately, this crate does not
|
||||||
//! support non-static closures, so we've implemented the core logic of `ValueA` in this module.
|
//! support non-static closures, so we've implemented the core logic of `ValueA` in this module.
|
||||||
|
|
||||||
|
use smallbox::{smallbox, space::S16, SmallBox};
|
||||||
|
|
||||||
use crate::{innerlude::VNode, ScopeState};
|
use crate::{innerlude::VNode, ScopeState};
|
||||||
use std::mem;
|
|
||||||
|
|
||||||
/// A concrete type provider for closures that build [`VNode`] structures.
|
/// A concrete type provider for closures that build [`VNode`] structures.
|
||||||
///
|
///
|
||||||
@@ -24,14 +25,7 @@ use std::mem;
|
|||||||
/// LazyNodes::new(|f| f.element("div", [], [], [] None))
|
/// LazyNodes::new(|f| f.element("div", [], [], [] None))
|
||||||
/// ```
|
/// ```
|
||||||
pub struct LazyNodes<'a, 'b> {
|
pub struct LazyNodes<'a, 'b> {
|
||||||
inner: StackNodeStorage<'a, 'b>,
|
inner: SmallBox<dyn FnMut(&'a ScopeState) -> VNode<'a> + 'b, S16>,
|
||||||
}
|
|
||||||
|
|
||||||
type StackHeapSize = [usize; 16];
|
|
||||||
|
|
||||||
enum StackNodeStorage<'a, 'b> {
|
|
||||||
Stack(LazyStack),
|
|
||||||
Heap(Box<dyn FnMut(Option<&'a ScopeState>) -> Option<VNode<'a>> + 'b>),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'b> LazyNodes<'a, 'b> {
|
impl<'a, 'b> LazyNodes<'a, 'b> {
|
||||||
@@ -44,114 +38,11 @@ impl<'a, 'b> LazyNodes<'a, 'b> {
|
|||||||
// there's no way to call FnOnce without a box, so we need to store it in a slot and use static dispatch
|
// there's no way to call FnOnce without a box, so we need to store it in a slot and use static dispatch
|
||||||
let mut slot = Some(val);
|
let mut slot = Some(val);
|
||||||
|
|
||||||
let val = move |fac: Option<&'a ScopeState>| {
|
|
||||||
fac.map(
|
|
||||||
slot.take()
|
|
||||||
.expect("LazyNodes closure to be called only once"),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
// miri does not know how to work with mucking directly into bytes
|
|
||||||
// just use a heap allocated type when miri is running
|
|
||||||
if cfg!(miri) {
|
|
||||||
Self {
|
|
||||||
inner: StackNodeStorage::Heap(Box::new(val)),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
unsafe { LazyNodes::new_inner(val) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new [`LazyNodes`] closure, but force it onto the heap.
|
|
||||||
pub fn new_boxed<F>(inner: F) -> Self
|
|
||||||
where
|
|
||||||
F: FnOnce(&'a ScopeState) -> VNode<'a> + 'b,
|
|
||||||
{
|
|
||||||
// there's no way to call FnOnce without a box, so we need to store it in a slot and use static dispatch
|
|
||||||
let mut slot = Some(inner);
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
inner: StackNodeStorage::Heap(Box::new(move |fac: Option<&'a ScopeState>| {
|
inner: smallbox!(move |f| {
|
||||||
fac.map(
|
let val = slot.take().expect("cannot call LazyNodes twice");
|
||||||
slot.take()
|
val(f)
|
||||||
.expect("LazyNodes closure to be called only once"),
|
}),
|
||||||
)
|
|
||||||
})),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe fn new_inner<F>(val: F) -> Self
|
|
||||||
where
|
|
||||||
F: FnMut(Option<&'a ScopeState>) -> Option<VNode<'a>> + 'b,
|
|
||||||
{
|
|
||||||
let mut ptr: *const _ = &val as &dyn FnMut(Option<&'a ScopeState>) -> Option<VNode<'a>>;
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
ptr as *const u8, &val as *const _ as *const u8,
|
|
||||||
"MISUSE: Closure returned different pointer"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
std::mem::size_of_val(&*ptr),
|
|
||||||
std::mem::size_of::<F>(),
|
|
||||||
"MISUSE: Closure returned a subset pointer"
|
|
||||||
);
|
|
||||||
|
|
||||||
let words = ptr_as_slice(&mut ptr);
|
|
||||||
assert!(
|
|
||||||
words[0] == &val as *const _ as usize,
|
|
||||||
"BUG: Pointer layout is not (data_ptr, info...)"
|
|
||||||
);
|
|
||||||
|
|
||||||
// - Ensure that Self is aligned same as data requires
|
|
||||||
assert!(
|
|
||||||
std::mem::align_of::<F>() <= std::mem::align_of::<Self>(),
|
|
||||||
"TODO: Enforce alignment >{} (requires {})",
|
|
||||||
std::mem::align_of::<Self>(),
|
|
||||||
std::mem::align_of::<F>()
|
|
||||||
);
|
|
||||||
|
|
||||||
let info = &words[1..];
|
|
||||||
let data = words[0] as *mut ();
|
|
||||||
let size = mem::size_of::<F>();
|
|
||||||
|
|
||||||
let stored_size = info.len() * mem::size_of::<usize>() + size;
|
|
||||||
let max_size = mem::size_of::<StackHeapSize>();
|
|
||||||
|
|
||||||
if stored_size > max_size {
|
|
||||||
Self {
|
|
||||||
inner: StackNodeStorage::Heap(Box::new(val)),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let mut buf: StackHeapSize = StackHeapSize::default();
|
|
||||||
|
|
||||||
assert!(info.len() + round_to_words(size) <= buf.as_ref().len());
|
|
||||||
|
|
||||||
// Place pointer information at the end of the region
|
|
||||||
// - Allows the data to be at the start for alignment purposes
|
|
||||||
{
|
|
||||||
let info_ofs = buf.as_ref().len() - info.len();
|
|
||||||
let info_dst = &mut buf.as_mut()[info_ofs..];
|
|
||||||
for (d, v) in Iterator::zip(info_dst.iter_mut(), info.iter()) {
|
|
||||||
*d = *v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let src_ptr = data as *const u8;
|
|
||||||
let dataptr = buf.as_mut_ptr().cast::<u8>();
|
|
||||||
|
|
||||||
for i in 0..size {
|
|
||||||
*dataptr.add(i) = *src_ptr.add(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::mem::forget(val);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
inner: StackNodeStorage::Stack(LazyStack {
|
|
||||||
_align: [],
|
|
||||||
buf,
|
|
||||||
dropped: false,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,88 +54,7 @@ impl<'a, 'b> LazyNodes<'a, 'b> {
|
|||||||
/// let node = f.call(cac);
|
/// let node = f.call(cac);
|
||||||
/// ```
|
/// ```
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn call(self, f: &'a ScopeState) -> VNode<'a> {
|
pub fn call(mut self, f: &'a ScopeState) -> VNode<'a> {
|
||||||
match self.inner {
|
(self.inner)(f)
|
||||||
StackNodeStorage::Heap(mut lazy) => {
|
|
||||||
lazy(Some(f)).expect("Closure should not be called twice")
|
|
||||||
}
|
|
||||||
StackNodeStorage::Stack(mut stack) => stack.call(f),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct LazyStack {
|
|
||||||
_align: [u64; 0],
|
|
||||||
buf: StackHeapSize,
|
|
||||||
dropped: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LazyStack {
|
|
||||||
fn call<'a>(&mut self, f: &'a ScopeState) -> VNode<'a> {
|
|
||||||
let LazyStack { buf, .. } = self;
|
|
||||||
let data = buf.as_ref();
|
|
||||||
|
|
||||||
let info_size =
|
|
||||||
mem::size_of::<*mut dyn FnMut(Option<&'a ScopeState>) -> Option<VNode<'a>>>()
|
|
||||||
/ mem::size_of::<usize>()
|
|
||||||
- 1;
|
|
||||||
|
|
||||||
let info_ofs = data.len() - info_size;
|
|
||||||
|
|
||||||
let g: *mut dyn FnMut(Option<&'a ScopeState>) -> Option<VNode<'a>> =
|
|
||||||
unsafe { make_fat_ptr(data[..].as_ptr() as usize, &data[info_ofs..]) };
|
|
||||||
|
|
||||||
self.dropped = true;
|
|
||||||
|
|
||||||
let clos = unsafe { &mut *g };
|
|
||||||
clos(Some(f)).unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Drop for LazyStack {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
if !self.dropped {
|
|
||||||
let LazyStack { buf, .. } = self;
|
|
||||||
let data = buf.as_ref();
|
|
||||||
|
|
||||||
let info_size =
|
|
||||||
mem::size_of::<*mut dyn FnMut(Option<&ScopeState>) -> Option<VNode<'_>>>()
|
|
||||||
/ mem::size_of::<usize>()
|
|
||||||
- 1;
|
|
||||||
|
|
||||||
let info_ofs = data.len() - info_size;
|
|
||||||
|
|
||||||
let g: *mut dyn FnMut(Option<&ScopeState>) -> Option<VNode<'_>> =
|
|
||||||
unsafe { make_fat_ptr(data[..].as_ptr() as usize, &data[info_ofs..]) };
|
|
||||||
|
|
||||||
self.dropped = true;
|
|
||||||
|
|
||||||
let clos = unsafe { &mut *g };
|
|
||||||
clos(None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Obtain mutable access to a pointer's words
|
|
||||||
fn ptr_as_slice<T>(ptr: &mut T) -> &mut [usize] {
|
|
||||||
assert!(mem::size_of::<T>() % mem::size_of::<usize>() == 0);
|
|
||||||
let words = mem::size_of::<T>() / mem::size_of::<usize>();
|
|
||||||
// SAFE: Points to valid memory (a raw pointer)
|
|
||||||
unsafe { core::slice::from_raw_parts_mut(ptr as *mut _ as *mut usize, words) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Re-construct a fat pointer
|
|
||||||
unsafe fn make_fat_ptr<T: ?Sized>(data_ptr: usize, meta_vals: &[usize]) -> *mut T {
|
|
||||||
let mut rv = mem::MaybeUninit::<*mut T>::uninit();
|
|
||||||
{
|
|
||||||
let s = ptr_as_slice(&mut rv);
|
|
||||||
s[0] = data_ptr;
|
|
||||||
s[1..].copy_from_slice(meta_vals);
|
|
||||||
}
|
|
||||||
let rv = rv.assume_init();
|
|
||||||
assert_eq!(rv as *const (), data_ptr as *const ());
|
|
||||||
rv
|
|
||||||
}
|
|
||||||
|
|
||||||
fn round_to_words(len: usize) -> usize {
|
|
||||||
(len + mem::size_of::<usize>() - 1) / mem::size_of::<usize>()
|
|
||||||
}
|
|
||||||
@@ -81,9 +81,9 @@ pub use crate::innerlude::{
|
|||||||
/// This includes types like [`Scope`], [`Element`], and [`Component`].
|
/// This includes types like [`Scope`], [`Element`], and [`Component`].
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
pub use crate::innerlude::{
|
pub use crate::innerlude::{
|
||||||
fc_to_builder, Element, Event, EventHandler, Fragment, LazyNodes, Properties, Scope,
|
fc_to_builder, Component, Element, Event, EventHandler, Fragment, LazyNodes, Properties,
|
||||||
ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, VNode,
|
Scope, ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode,
|
||||||
VirtualDom,
|
VNode, VirtualDom,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use fxhash::FxHashSet;
|
use rustc_hash::FxHashSet;
|
||||||
|
|
||||||
use crate::{arena::ElementId, ScopeId, Template};
|
use crate::{arena::ElementId, ScopeId, Template};
|
||||||
|
|
||||||
|
|||||||
@@ -441,6 +441,12 @@ impl<'a> IntoDynNode<'a> for VNode<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a> IntoDynNode<'a> for DynamicNode<'a> {
|
||||||
|
fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// An element that's an error is currently lost into the ether
|
// An element that's an error is currently lost into the ether
|
||||||
impl<'a> IntoDynNode<'a> for Element<'a> {
|
impl<'a> IntoDynNode<'a> for Element<'a> {
|
||||||
fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> {
|
fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use crate::{
|
|||||||
scopes::{ScopeId, ScopeState},
|
scopes::{ScopeId, ScopeState},
|
||||||
virtual_dom::VirtualDom,
|
virtual_dom::VirtualDom,
|
||||||
};
|
};
|
||||||
|
use bumpalo::Bump;
|
||||||
use futures_util::FutureExt;
|
use futures_util::FutureExt;
|
||||||
use std::{
|
use std::{
|
||||||
mem,
|
mem,
|
||||||
@@ -34,8 +35,8 @@ impl VirtualDom {
|
|||||||
props: Some(props),
|
props: Some(props),
|
||||||
name,
|
name,
|
||||||
placeholder: Default::default(),
|
placeholder: Default::default(),
|
||||||
node_arena_1: BumpFrame::new(50),
|
node_arena_1: BumpFrame::new(0),
|
||||||
node_arena_2: BumpFrame::new(50),
|
node_arena_2: BumpFrame::new(0),
|
||||||
spawned_tasks: Default::default(),
|
spawned_tasks: Default::default(),
|
||||||
render_cnt: Default::default(),
|
render_cnt: Default::default(),
|
||||||
hook_arena: Default::default(),
|
hook_arena: Default::default(),
|
||||||
@@ -62,7 +63,13 @@ impl VirtualDom {
|
|||||||
let mut new_nodes = unsafe {
|
let mut new_nodes = unsafe {
|
||||||
let scope = self.scopes[scope_id.0].as_mut();
|
let scope = self.scopes[scope_id.0].as_mut();
|
||||||
|
|
||||||
scope.previous_frame_mut().bump.reset();
|
// if this frame hasn't been intialized yet, we can guess the size of the next frame to be more efficient
|
||||||
|
if scope.previous_frame().bump.allocated_bytes() == 0 {
|
||||||
|
scope.previous_frame_mut().bump =
|
||||||
|
Bump::with_capacity(scope.current_frame().bump.allocated_bytes());
|
||||||
|
} else {
|
||||||
|
scope.previous_frame_mut().bump.reset();
|
||||||
|
}
|
||||||
|
|
||||||
// Make sure to reset the hook counter so we give out hooks in the right order
|
// Make sure to reset the hook counter so we give out hooks in the right order
|
||||||
scope.hook_idx.set(0);
|
scope.hook_idx.set(0);
|
||||||
@@ -128,8 +135,8 @@ impl VirtualDom {
|
|||||||
let frame = scope.previous_frame();
|
let frame = scope.previous_frame();
|
||||||
|
|
||||||
// set the new head of the bump frame
|
// set the new head of the bump frame
|
||||||
let alloced = &*frame.bump.alloc(new_nodes);
|
let allocated = &*frame.bump.alloc(new_nodes);
|
||||||
frame.node.set(alloced);
|
frame.node.set(allocated);
|
||||||
|
|
||||||
// And move the render generation forward by one
|
// And move the render generation forward by one
|
||||||
scope.render_cnt.set(scope.render_cnt.get() + 1);
|
scope.render_cnt.set(scope.render_cnt.get() + 1);
|
||||||
@@ -141,6 +148,6 @@ impl VirtualDom {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// rebind the lifetime now that its stored internally
|
// rebind the lifetime now that its stored internally
|
||||||
unsafe { mem::transmute(alloced) }
|
unsafe { mem::transmute(allocated) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,10 +10,10 @@ use crate::{
|
|||||||
Attribute, AttributeValue, Element, Event, Properties, TaskId,
|
Attribute, AttributeValue, Element, Event, Properties, TaskId,
|
||||||
};
|
};
|
||||||
use bumpalo::{boxed::Box as BumpBox, Bump};
|
use bumpalo::{boxed::Box as BumpBox, Bump};
|
||||||
|
use rustc_hash::{FxHashMap, FxHashSet};
|
||||||
use std::{
|
use std::{
|
||||||
any::{Any, TypeId},
|
any::{Any, TypeId},
|
||||||
cell::{Cell, RefCell},
|
cell::{Cell, RefCell},
|
||||||
collections::{HashMap, HashSet},
|
|
||||||
fmt::Arguments,
|
fmt::Arguments,
|
||||||
future::Future,
|
future::Future,
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
@@ -82,10 +82,10 @@ pub struct ScopeState {
|
|||||||
pub(crate) hook_list: RefCell<Vec<*mut dyn Any>>,
|
pub(crate) hook_list: RefCell<Vec<*mut dyn Any>>,
|
||||||
pub(crate) hook_idx: Cell<usize>,
|
pub(crate) hook_idx: Cell<usize>,
|
||||||
|
|
||||||
pub(crate) shared_contexts: RefCell<HashMap<TypeId, Box<dyn Any>>>,
|
pub(crate) shared_contexts: RefCell<FxHashMap<TypeId, Box<dyn Any>>>,
|
||||||
|
|
||||||
pub(crate) tasks: Rc<Scheduler>,
|
pub(crate) tasks: Rc<Scheduler>,
|
||||||
pub(crate) spawned_tasks: HashSet<TaskId>,
|
pub(crate) spawned_tasks: FxHashSet<TaskId>,
|
||||||
|
|
||||||
pub(crate) props: Option<Box<dyn AnyProps<'static>>>,
|
pub(crate) props: Option<Box<dyn AnyProps<'static>>>,
|
||||||
pub(crate) placeholder: Cell<Option<ElementId>>,
|
pub(crate) placeholder: Cell<Option<ElementId>>,
|
||||||
|
|||||||
@@ -14,15 +14,9 @@ use crate::{
|
|||||||
AttributeValue, Element, Event, Scope, SuspenseContext,
|
AttributeValue, Element, Event, Scope, SuspenseContext,
|
||||||
};
|
};
|
||||||
use futures_util::{pin_mut, StreamExt};
|
use futures_util::{pin_mut, StreamExt};
|
||||||
|
use rustc_hash::FxHashMap;
|
||||||
use slab::Slab;
|
use slab::Slab;
|
||||||
use std::{
|
use std::{any::Any, borrow::BorrowMut, cell::Cell, collections::BTreeSet, future::Future, rc::Rc};
|
||||||
any::Any,
|
|
||||||
borrow::BorrowMut,
|
|
||||||
cell::Cell,
|
|
||||||
collections::{BTreeSet, HashMap},
|
|
||||||
future::Future,
|
|
||||||
rc::Rc,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// A virtual node system that progresses user events and diffs UI trees.
|
/// A virtual node system that progresses user events and diffs UI trees.
|
||||||
///
|
///
|
||||||
@@ -148,7 +142,7 @@ use std::{
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub struct VirtualDom {
|
pub struct VirtualDom {
|
||||||
pub(crate) templates: HashMap<TemplateId, Template<'static>>,
|
pub(crate) templates: FxHashMap<TemplateId, Template<'static>>,
|
||||||
pub(crate) scopes: Slab<Box<ScopeState>>,
|
pub(crate) scopes: Slab<Box<ScopeState>>,
|
||||||
pub(crate) dirty_scopes: BTreeSet<DirtyScope>,
|
pub(crate) dirty_scopes: BTreeSet<DirtyScope>,
|
||||||
pub(crate) scheduler: Rc<Scheduler>,
|
pub(crate) scheduler: Rc<Scheduler>,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use futures_channel::mpsc::{unbounded, UnboundedSender};
|
|||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
#[cfg(target_os = "ios")]
|
#[cfg(target_os = "ios")]
|
||||||
use objc::runtime::Object;
|
use objc::runtime::Object;
|
||||||
|
use serde_json::Value;
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
@@ -19,6 +20,7 @@ use wry::{
|
|||||||
|
|
||||||
pub(super) struct DesktopController {
|
pub(super) struct DesktopController {
|
||||||
pub(super) webviews: HashMap<WindowId, WebView>,
|
pub(super) webviews: HashMap<WindowId, WebView>,
|
||||||
|
pub(super) eval_sender: tokio::sync::mpsc::UnboundedSender<Value>,
|
||||||
pub(super) pending_edits: Arc<Mutex<Vec<String>>>,
|
pub(super) pending_edits: Arc<Mutex<Vec<String>>>,
|
||||||
pub(super) quit_app_on_close: bool,
|
pub(super) quit_app_on_close: bool,
|
||||||
pub(super) is_ready: Arc<AtomicBool>,
|
pub(super) is_ready: Arc<AtomicBool>,
|
||||||
@@ -43,6 +45,7 @@ impl DesktopController {
|
|||||||
|
|
||||||
let pending_edits = edit_queue.clone();
|
let pending_edits = edit_queue.clone();
|
||||||
let desktop_context_proxy = proxy.clone();
|
let desktop_context_proxy = proxy.clone();
|
||||||
|
let (eval_sender, eval_reciever) = tokio::sync::mpsc::unbounded_channel::<Value>();
|
||||||
|
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
// We create the runtime as multithreaded, so you can still "tokio::spawn" onto multiple threads
|
// We create the runtime as multithreaded, so you can still "tokio::spawn" onto multiple threads
|
||||||
@@ -54,7 +57,7 @@ impl DesktopController {
|
|||||||
|
|
||||||
runtime.block_on(async move {
|
runtime.block_on(async move {
|
||||||
let mut dom = VirtualDom::new_with_props(root, props)
|
let mut dom = VirtualDom::new_with_props(root, props)
|
||||||
.with_root_context(DesktopContext::new(desktop_context_proxy));
|
.with_root_context(DesktopContext::new(desktop_context_proxy, eval_reciever));
|
||||||
{
|
{
|
||||||
let edits = dom.rebuild();
|
let edits = dom.rebuild();
|
||||||
let mut queue = edit_queue.lock().unwrap();
|
let mut queue = edit_queue.lock().unwrap();
|
||||||
@@ -88,6 +91,7 @@ impl DesktopController {
|
|||||||
|
|
||||||
Self {
|
Self {
|
||||||
pending_edits,
|
pending_edits,
|
||||||
|
eval_sender,
|
||||||
webviews: HashMap::new(),
|
webviews: HashMap::new(),
|
||||||
is_ready: Arc::new(AtomicBool::new(false)),
|
is_ready: Arc::new(AtomicBool::new(false)),
|
||||||
quit_app_on_close: true,
|
quit_app_on_close: true,
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ use std::rc::Rc;
|
|||||||
|
|
||||||
use crate::controller::DesktopController;
|
use crate::controller::DesktopController;
|
||||||
use dioxus_core::ScopeState;
|
use dioxus_core::ScopeState;
|
||||||
|
use serde::de::Error;
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::future::Future;
|
||||||
|
use std::future::IntoFuture;
|
||||||
|
use std::pin::Pin;
|
||||||
use wry::application::event_loop::ControlFlow;
|
use wry::application::event_loop::ControlFlow;
|
||||||
use wry::application::event_loop::EventLoopProxy;
|
use wry::application::event_loop::EventLoopProxy;
|
||||||
#[cfg(target_os = "ios")]
|
#[cfg(target_os = "ios")]
|
||||||
@@ -19,16 +24,6 @@ pub fn use_window(cx: &ScopeState) -> &DesktopContext {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a closure that executes any JavaScript in the WebView context.
|
|
||||||
pub fn use_eval(cx: &ScopeState) -> &Rc<dyn Fn(String)> {
|
|
||||||
let desktop = use_window(cx);
|
|
||||||
|
|
||||||
&*cx.use_hook(|| {
|
|
||||||
let desktop = desktop.clone();
|
|
||||||
Rc::new(move |script| desktop.eval(script))
|
|
||||||
} as Rc<dyn Fn(String)>)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An imperative interface to the current window.
|
/// An imperative interface to the current window.
|
||||||
///
|
///
|
||||||
/// To get a handle to the current window, use the [`use_window`] hook.
|
/// To get a handle to the current window, use the [`use_window`] hook.
|
||||||
@@ -45,11 +40,18 @@ pub fn use_eval(cx: &ScopeState) -> &Rc<dyn Fn(String)> {
|
|||||||
pub struct DesktopContext {
|
pub struct DesktopContext {
|
||||||
/// The wry/tao proxy to the current window
|
/// The wry/tao proxy to the current window
|
||||||
pub proxy: ProxyType,
|
pub proxy: ProxyType,
|
||||||
|
pub(super) eval_reciever: Rc<tokio::sync::Mutex<tokio::sync::mpsc::UnboundedReceiver<Value>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DesktopContext {
|
impl DesktopContext {
|
||||||
pub(crate) fn new(proxy: ProxyType) -> Self {
|
pub(crate) fn new(
|
||||||
Self { proxy }
|
proxy: ProxyType,
|
||||||
|
eval_reciever: tokio::sync::mpsc::UnboundedReceiver<Value>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
proxy,
|
||||||
|
eval_reciever: Rc::new(tokio::sync::Mutex::new(eval_reciever)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// trigger the drag-window event
|
/// trigger the drag-window event
|
||||||
@@ -242,6 +244,18 @@ impl DesktopController {
|
|||||||
Resizable(state) => window.set_resizable(state),
|
Resizable(state) => window.set_resizable(state),
|
||||||
AlwaysOnTop(state) => window.set_always_on_top(state),
|
AlwaysOnTop(state) => window.set_always_on_top(state),
|
||||||
|
|
||||||
|
Eval(code) => {
|
||||||
|
let script = format!(
|
||||||
|
r#"window.ipc.postMessage(JSON.stringify({{"method":"eval_result", params: (function(){{
|
||||||
|
{}
|
||||||
|
}})()}}));"#,
|
||||||
|
code
|
||||||
|
);
|
||||||
|
if let Err(e) = webview.evaluate_script(&script) {
|
||||||
|
// we can't panic this error.
|
||||||
|
log::warn!("Eval script error: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
CursorVisible(state) => window.set_cursor_visible(state),
|
CursorVisible(state) => window.set_cursor_visible(state),
|
||||||
CursorGrab(state) => {
|
CursorGrab(state) => {
|
||||||
let _ = window.set_cursor_grab(state);
|
let _ = window.set_cursor_grab(state);
|
||||||
@@ -265,13 +279,6 @@ impl DesktopController {
|
|||||||
log::warn!("Devtools are disabled in release builds");
|
log::warn!("Devtools are disabled in release builds");
|
||||||
}
|
}
|
||||||
|
|
||||||
Eval(code) => {
|
|
||||||
if let Err(e) = webview.evaluate_script(code.as_str()) {
|
|
||||||
// we can't panic this error.
|
|
||||||
log::warn!("Eval script error: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "ios")]
|
#[cfg(target_os = "ios")]
|
||||||
PushView(view) => unsafe {
|
PushView(view) => unsafe {
|
||||||
use objc::runtime::Object;
|
use objc::runtime::Object;
|
||||||
@@ -301,6 +308,39 @@ impl DesktopController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get a closure that executes any JavaScript in the WebView context.
|
||||||
|
pub fn use_eval<S: std::string::ToString>(cx: &ScopeState) -> &dyn Fn(S) -> EvalResult {
|
||||||
|
let desktop = use_window(cx).clone();
|
||||||
|
cx.use_hook(|| {
|
||||||
|
move |script| {
|
||||||
|
desktop.eval(script);
|
||||||
|
let recv = desktop.eval_reciever.clone();
|
||||||
|
EvalResult { reciever: recv }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A future that resolves to the result of a JavaScript evaluation.
|
||||||
|
pub struct EvalResult {
|
||||||
|
reciever: Rc<tokio::sync::Mutex<tokio::sync::mpsc::UnboundedReceiver<serde_json::Value>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoFuture for EvalResult {
|
||||||
|
type Output = Result<serde_json::Value, serde_json::Error>;
|
||||||
|
|
||||||
|
type IntoFuture = Pin<Box<dyn Future<Output = Result<serde_json::Value, serde_json::Error>>>>;
|
||||||
|
|
||||||
|
fn into_future(self) -> Self::IntoFuture {
|
||||||
|
Box::pin(async move {
|
||||||
|
let mut reciever = self.reciever.lock().await;
|
||||||
|
match reciever.recv().await {
|
||||||
|
Some(result) => Ok(result),
|
||||||
|
None => Err(serde_json::Error::custom("No result returned")),
|
||||||
|
}
|
||||||
|
}) as Pin<Box<dyn Future<Output = Result<serde_json::Value, serde_json::Error>>>>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "ios")]
|
#[cfg(target_os = "ios")]
|
||||||
fn is_main_thread() -> bool {
|
fn is_main_thread() -> bool {
|
||||||
use objc::runtime::{Class, BOOL, NO};
|
use objc::runtime::{Class, BOOL, NO};
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ use std::sync::atomic::AtomicBool;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use desktop_context::UserWindowEvent;
|
use desktop_context::UserWindowEvent;
|
||||||
pub use desktop_context::{use_eval, use_window, DesktopContext};
|
pub use desktop_context::{use_eval, use_window, DesktopContext, EvalResult};
|
||||||
use futures_channel::mpsc::UnboundedSender;
|
use futures_channel::mpsc::UnboundedSender;
|
||||||
pub use wry;
|
pub use wry;
|
||||||
pub use wry::application as tao;
|
pub use wry::application as tao;
|
||||||
@@ -142,6 +142,7 @@ impl DesktopController {
|
|||||||
event_loop,
|
event_loop,
|
||||||
self.is_ready.clone(),
|
self.is_ready.clone(),
|
||||||
self.proxy.clone(),
|
self.proxy.clone(),
|
||||||
|
self.eval_sender.clone(),
|
||||||
self.event_tx.clone(),
|
self.event_tx.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -154,6 +155,7 @@ fn build_webview(
|
|||||||
event_loop: &tao::event_loop::EventLoopWindowTarget<UserWindowEvent>,
|
event_loop: &tao::event_loop::EventLoopWindowTarget<UserWindowEvent>,
|
||||||
is_ready: Arc<AtomicBool>,
|
is_ready: Arc<AtomicBool>,
|
||||||
proxy: tao::event_loop::EventLoopProxy<UserWindowEvent>,
|
proxy: tao::event_loop::EventLoopProxy<UserWindowEvent>,
|
||||||
|
eval_sender: tokio::sync::mpsc::UnboundedSender<serde_json::Value>,
|
||||||
event_tx: UnboundedSender<serde_json::Value>,
|
event_tx: UnboundedSender<serde_json::Value>,
|
||||||
) -> wry::webview::WebView {
|
) -> wry::webview::WebView {
|
||||||
let builder = cfg.window.clone();
|
let builder = cfg.window.clone();
|
||||||
@@ -183,6 +185,10 @@ fn build_webview(
|
|||||||
.with_ipc_handler(move |_window: &Window, payload: String| {
|
.with_ipc_handler(move |_window: &Window, payload: String| {
|
||||||
parse_ipc_message(&payload)
|
parse_ipc_message(&payload)
|
||||||
.map(|message| match message.method() {
|
.map(|message| match message.method() {
|
||||||
|
"eval_result" => {
|
||||||
|
let result = message.params();
|
||||||
|
eval_sender.send(result).unwrap();
|
||||||
|
}
|
||||||
"user_event" => {
|
"user_event" => {
|
||||||
_ = event_tx.unbounded_send(message.params());
|
_ = event_tx.unbounded_send(message.params());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,10 +104,10 @@ fn get_asset_root() -> Option<PathBuf> {
|
|||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
let bundle = core_foundation::bundle::CFBundle::main_bundle();
|
let bundle = core_foundation::bundle::CFBundle::main_bundle();
|
||||||
let bundle_path = dbg!(bundle.path()?);
|
let bundle_path = bundle.path()?;
|
||||||
let resources_path = dbg!(bundle.resources_path()?);
|
let resources_path = bundle.resources_path()?;
|
||||||
let absolute_resources_root = dbg!(bundle_path.join(resources_path));
|
let absolute_resources_root = bundle_path.join(resources_path);
|
||||||
let canonical_resources_root = dbg!(dunce::canonicalize(absolute_resources_root).ok()?);
|
let canonical_resources_root = dunce::canonicalize(absolute_resources_root).ok()?;
|
||||||
|
|
||||||
return Some(canonical_resources_root);
|
return Some(canonical_resources_root);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#![doc = include_str!("../../../notes/README.md")]
|
|
||||||
|
|
||||||
pub use dioxus_core as core;
|
pub use dioxus_core as core;
|
||||||
|
|
||||||
#[cfg(feature = "hooks")]
|
#[cfg(feature = "hooks")]
|
||||||
|
|||||||
@@ -17,8 +17,10 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
|
|||||||
wasm-bindgen = { version = "0.2.79", optional = true }
|
wasm-bindgen = { version = "0.2.79", optional = true }
|
||||||
js-sys = { version = "0.3.56", optional = true }
|
js-sys = { version = "0.3.56", optional = true }
|
||||||
web-sys = { version = "0.3.56", optional = true, features = ["Element", "Node"] }
|
web-sys = { version = "0.3.56", optional = true, features = ["Element", "Node"] }
|
||||||
|
sledgehammer_bindgen = { version = "0.1.3", optional = true }
|
||||||
|
sledgehammer_utils = { version = "0.1.0", optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
web = ["wasm-bindgen", "js-sys", "web-sys"]
|
web = ["wasm-bindgen", "js-sys", "web-sys"]
|
||||||
|
sledgehammer = ["wasm-bindgen", "js-sys", "web-sys", "sledgehammer_bindgen", "sledgehammer_utils"]
|
||||||
@@ -17,9 +17,6 @@ extern "C" {
|
|||||||
#[wasm_bindgen(method)]
|
#[wasm_bindgen(method)]
|
||||||
pub fn MountToRoot(this: &Interpreter);
|
pub fn MountToRoot(this: &Interpreter);
|
||||||
|
|
||||||
#[wasm_bindgen(method)]
|
|
||||||
pub fn AppendChildren(this: &Interpreter, m: u32, id: u32);
|
|
||||||
|
|
||||||
#[wasm_bindgen(method)]
|
#[wasm_bindgen(method)]
|
||||||
pub fn AssignId(this: &Interpreter, path: &[u8], id: u32);
|
pub fn AssignId(this: &Interpreter, path: &[u8], id: u32);
|
||||||
|
|
||||||
@@ -76,4 +73,7 @@ extern "C" {
|
|||||||
|
|
||||||
#[wasm_bindgen(method)]
|
#[wasm_bindgen(method)]
|
||||||
pub fn PushRoot(this: &Interpreter, id: u32);
|
pub fn PushRoot(this: &Interpreter, id: u32);
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
pub fn AppendChildren(this: &Interpreter, id: u32, m: u32);
|
||||||
}
|
}
|
||||||
@@ -90,7 +90,8 @@ export class Interpreter {
|
|||||||
this.stack.pop();
|
this.stack.pop();
|
||||||
}
|
}
|
||||||
AppendChildren(many) {
|
AppendChildren(many) {
|
||||||
let root = this.stack[this.stack.length - (1 + many)];
|
// let root = this.nodes[id];
|
||||||
|
let root = this.stack[this.stack.length - 1 - many];
|
||||||
let to_add = this.stack.splice(this.stack.length - many);
|
let to_add = this.stack.splice(this.stack.length - many);
|
||||||
for (let i = 0; i < many; i++) {
|
for (let i = 0; i < many; i++) {
|
||||||
root.appendChild(to_add[i]);
|
root.appendChild(to_add[i]);
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
pub static INTERPRETER_JS: &str = include_str!("./interpreter.js");
|
pub static INTERPRETER_JS: &str = include_str!("./interpreter.js");
|
||||||
|
|
||||||
|
#[cfg(feature = "sledgehammer")]
|
||||||
|
mod sledgehammer_bindings;
|
||||||
|
#[cfg(feature = "sledgehammer")]
|
||||||
|
pub use sledgehammer_bindings::*;
|
||||||
|
|
||||||
#[cfg(feature = "web")]
|
#[cfg(feature = "web")]
|
||||||
mod bindings;
|
mod bindings;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,231 @@
|
|||||||
|
use js_sys::Function;
|
||||||
|
use sledgehammer_bindgen::bindgen;
|
||||||
|
use web_sys::Node;
|
||||||
|
|
||||||
|
#[bindgen]
|
||||||
|
mod js {
|
||||||
|
const JS: &str = r#"
|
||||||
|
class ListenerMap {
|
||||||
|
constructor(root) {
|
||||||
|
// bubbling events can listen at the root element
|
||||||
|
this.global = {};
|
||||||
|
// non bubbling events listen at the element the listener was created at
|
||||||
|
this.local = {};
|
||||||
|
this.root = null;
|
||||||
|
this.handler = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
create(event_name, element, bubbles) {
|
||||||
|
if (bubbles) {
|
||||||
|
if (this.global[event_name] === undefined) {
|
||||||
|
this.global[event_name] = {};
|
||||||
|
this.global[event_name].active = 1;
|
||||||
|
this.root.addEventListener(event_name, this.handler);
|
||||||
|
} else {
|
||||||
|
this.global[event_name].active++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const id = element.getAttribute("data-dioxus-id");
|
||||||
|
if (!this.local[id]) {
|
||||||
|
this.local[id] = {};
|
||||||
|
}
|
||||||
|
element.addEventListener(event_name, this.handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(element, event_name, bubbles) {
|
||||||
|
if (bubbles) {
|
||||||
|
this.global[event_name].active--;
|
||||||
|
if (this.global[event_name].active === 0) {
|
||||||
|
this.root.removeEventListener(event_name, this.global[event_name].callback);
|
||||||
|
delete this.global[event_name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const id = element.getAttribute("data-dioxus-id");
|
||||||
|
delete this.local[id][event_name];
|
||||||
|
if (this.local[id].length === 0) {
|
||||||
|
delete this.local[id];
|
||||||
|
}
|
||||||
|
element.removeEventListener(event_name, this.handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeAllNonBubbling(element) {
|
||||||
|
const id = element.getAttribute("data-dioxus-id");
|
||||||
|
delete this.local[id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function SetAttributeInner(node, field, value, ns) {
|
||||||
|
const name = field;
|
||||||
|
if (ns === "style") {
|
||||||
|
// ????? why do we need to do this
|
||||||
|
if (node.style === undefined) {
|
||||||
|
node.style = {};
|
||||||
|
}
|
||||||
|
node.style[name] = value;
|
||||||
|
} else if (ns !== null && ns !== undefined && ns !== "") {
|
||||||
|
node.setAttributeNS(ns, name, value);
|
||||||
|
} else {
|
||||||
|
switch (name) {
|
||||||
|
case "value":
|
||||||
|
if (value !== node.value) {
|
||||||
|
node.value = value;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "checked":
|
||||||
|
node.checked = value === "true";
|
||||||
|
break;
|
||||||
|
case "selected":
|
||||||
|
node.selected = value === "true";
|
||||||
|
break;
|
||||||
|
case "dangerous_inner_html":
|
||||||
|
node.innerHTML = value;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364
|
||||||
|
if (value === "false" && bool_attrs.hasOwnProperty(name)) {
|
||||||
|
node.removeAttribute(name);
|
||||||
|
} else {
|
||||||
|
node.setAttribute(name, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function LoadChild(ptr, len) {
|
||||||
|
// iterate through each number and get that child
|
||||||
|
node = stack[stack.length - 1];
|
||||||
|
ptr_end = ptr + len;
|
||||||
|
for (; ptr < ptr_end; ptr++) {
|
||||||
|
end = m.getUint8(ptr);
|
||||||
|
for (node = node.firstChild; end > 0; end--) {
|
||||||
|
node = node.nextSibling;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
const listeners = new ListenerMap();
|
||||||
|
let nodes = [];
|
||||||
|
let stack = [];
|
||||||
|
const templates = {};
|
||||||
|
let node, els, end, ptr_end, k;
|
||||||
|
export function save_template(nodes, tmpl_id) {
|
||||||
|
templates[tmpl_id] = nodes;
|
||||||
|
}
|
||||||
|
export function set_node(id, node) {
|
||||||
|
nodes[id] = node;
|
||||||
|
}
|
||||||
|
export function initilize(root, handler) {
|
||||||
|
listeners.handler = handler;
|
||||||
|
nodes = [root];
|
||||||
|
stack = [root];
|
||||||
|
listeners.root = root;
|
||||||
|
}
|
||||||
|
function AppendChildren(id, many){
|
||||||
|
root = nodes[id];
|
||||||
|
els = stack.splice(stack.length-many);
|
||||||
|
for (k = 0; k < many; k++) {
|
||||||
|
root.appendChild(els[k]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn save_template(nodes: Vec<Node>, tmpl_id: u32);
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn set_node(id: u32, node: Node);
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn initilize(root: Node, handler: &Function);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mount_to_root() {
|
||||||
|
"{AppendChildren(root, stack.length-1);}"
|
||||||
|
}
|
||||||
|
fn push_root(root: u32) {
|
||||||
|
"{stack.push(nodes[$root$]);}"
|
||||||
|
}
|
||||||
|
fn append_children(id: u32, many: u32) {
|
||||||
|
"{AppendChildren($id$, $many$);}"
|
||||||
|
}
|
||||||
|
fn pop_root() {
|
||||||
|
"{stack.pop();}"
|
||||||
|
}
|
||||||
|
fn replace_with(id: u32, n: u32) {
|
||||||
|
"{root = nodes[$id$]; els = stack.splice(stack.length-$n$); if (root.listening) { listeners.removeAllNonBubbling(root); } root.replaceWith(...els);}"
|
||||||
|
}
|
||||||
|
fn insert_after(id: u32, n: u32) {
|
||||||
|
"{nodes[$id$].after(...stack.splice(stack.length-$n$));}"
|
||||||
|
}
|
||||||
|
fn insert_before(id: u32, n: u32) {
|
||||||
|
"{nodes[$id$].before(...stack.splice(stack.length-$n$));}"
|
||||||
|
}
|
||||||
|
fn remove(id: u32) {
|
||||||
|
"{node = nodes[$id$]; if (node !== undefined) { if (node.listening) { listeners.removeAllNonBubbling(node); } node.remove(); }}"
|
||||||
|
}
|
||||||
|
fn create_raw_text(text: &str) {
|
||||||
|
"{stack.push(document.createTextNode($text$));}"
|
||||||
|
}
|
||||||
|
fn create_text_node(text: &str, id: u32) {
|
||||||
|
"{node = document.createTextNode($text$); nodes[$id$] = node; stack.push(node);}"
|
||||||
|
}
|
||||||
|
fn create_placeholder(id: u32) {
|
||||||
|
"{node = document.createElement('pre'); node.hidden = true; stack.push(node); nodes[$id$] = node;}"
|
||||||
|
}
|
||||||
|
fn new_event_listener(event_name: &str<u8, evt>, id: u32, bubbles: u8) {
|
||||||
|
r#"node = nodes[id]; if(node.listening){node.listening += 1;}else{node.listening = 1;} node.setAttribute('data-dioxus-id', `\${id}`); listeners.create($event_name$, node, $bubbles$);"#
|
||||||
|
}
|
||||||
|
fn remove_event_listener(event_name: &str<u8, evt>, id: u32, bubbles: u8) {
|
||||||
|
"{node = nodes[$id$]; node.listening -= 1; node.removeAttribute('data-dioxus-id'); listeners.remove(node, $event_name$, $bubbles$);}"
|
||||||
|
}
|
||||||
|
fn set_text(id: u32, text: &str) {
|
||||||
|
"{nodes[$id$].textContent = $text$;}"
|
||||||
|
}
|
||||||
|
fn set_attribute(id: u32, field: &str<u8, attr>, value: &str, ns: &str<u8, ns_cache>) {
|
||||||
|
"{node = nodes[$id$]; SetAttributeInner(node, $field$, $value$, $ns$);}"
|
||||||
|
}
|
||||||
|
fn remove_attribute(id: u32, field: &str<u8, attr>, ns: &str<u8, ns_cache>) {
|
||||||
|
r#"{name = $field$;
|
||||||
|
node = this.nodes[$id$];
|
||||||
|
if (ns == "style") {
|
||||||
|
node.style.removeProperty(name);
|
||||||
|
} else if (ns !== null && ns !== undefined && ns !== "") {
|
||||||
|
node.removeAttributeNS(ns, name);
|
||||||
|
} else if (name === "value") {
|
||||||
|
node.value = "";
|
||||||
|
} else if (name === "checked") {
|
||||||
|
node.checked = false;
|
||||||
|
} else if (name === "selected") {
|
||||||
|
node.selected = false;
|
||||||
|
} else if (name === "dangerous_inner_html") {
|
||||||
|
node.innerHTML = "";
|
||||||
|
} else {
|
||||||
|
node.removeAttribute(name);
|
||||||
|
}}"#
|
||||||
|
}
|
||||||
|
fn assign_id(ptr: u32, len: u8, id: u32) {
|
||||||
|
"{nodes[$id$] = LoadChild($ptr$, $len$);}"
|
||||||
|
}
|
||||||
|
fn hydrate_text(ptr: u32, len: u8, value: &str, id: u32) {
|
||||||
|
r#"{
|
||||||
|
node = LoadChild($ptr$, $len$);
|
||||||
|
if (node.nodeType == Node.TEXT_NODE) {
|
||||||
|
node.textContent = value;
|
||||||
|
} else {
|
||||||
|
let text = document.createTextNode(value);
|
||||||
|
node.replaceWith(text);
|
||||||
|
node = text;
|
||||||
|
}
|
||||||
|
nodes[$id$] = node;
|
||||||
|
}"#
|
||||||
|
}
|
||||||
|
fn replace_placeholder(ptr: u32, len: u8, n: u32) {
|
||||||
|
"{els = stack.splice(stack.length - $n$); node = LoadChild($ptr$, $len$); node.replaceWith(...els);}"
|
||||||
|
}
|
||||||
|
fn load_template(tmpl_id: u32, index: u32, id: u32) {
|
||||||
|
"{node = templates[$tmpl_id$][$index$].cloneNode(true); nodes[$id$] = node; stack.push(node);}"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -306,8 +306,7 @@ impl Member {
|
|||||||
+ field.ty.to_token_stream().to_string().as_str())
|
+ field.ty.to_token_stream().to_string().as_str())
|
||||||
.as_str(),
|
.as_str(),
|
||||||
Span::call_site(),
|
Span::call_site(),
|
||||||
)
|
),
|
||||||
.into(),
|
|
||||||
ident: field.ident.as_ref()?.clone(),
|
ident: field.ident.as_ref()?.clone(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,11 +102,7 @@ impl<S: State> RealDom<S> {
|
|||||||
self.tree.add_child(node_id, child_id);
|
self.tree.add_child(node_id, child_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_template_node(
|
fn create_template_node(&mut self, node: &TemplateNode) -> RealNodeId {
|
||||||
&mut self,
|
|
||||||
node: &TemplateNode,
|
|
||||||
mutations_vec: &mut FxHashMap<RealNodeId, NodeMask>,
|
|
||||||
) -> RealNodeId {
|
|
||||||
match node {
|
match node {
|
||||||
TemplateNode::Element {
|
TemplateNode::Element {
|
||||||
tag,
|
tag,
|
||||||
@@ -139,27 +135,18 @@ impl<S: State> RealDom<S> {
|
|||||||
});
|
});
|
||||||
let node_id = self.create_node(node);
|
let node_id = self.create_node(node);
|
||||||
for child in *children {
|
for child in *children {
|
||||||
let child_id = self.create_template_node(child, mutations_vec);
|
let child_id = self.create_template_node(child);
|
||||||
self.add_child(node_id, child_id);
|
self.add_child(node_id, child_id);
|
||||||
}
|
}
|
||||||
node_id
|
node_id
|
||||||
}
|
}
|
||||||
TemplateNode::Text { text } => {
|
TemplateNode::Text { text } => self.create_node(Node::new(NodeType::Text {
|
||||||
let node_id = self.create_node(Node::new(NodeType::Text {
|
text: text.to_string(),
|
||||||
text: text.to_string(),
|
})),
|
||||||
}));
|
TemplateNode::Dynamic { .. } => self.create_node(Node::new(NodeType::Placeholder)),
|
||||||
node_id
|
TemplateNode::DynamicText { .. } => self.create_node(Node::new(NodeType::Text {
|
||||||
}
|
text: String::new(),
|
||||||
TemplateNode::Dynamic { .. } => {
|
})),
|
||||||
let node_id = self.create_node(Node::new(NodeType::Placeholder));
|
|
||||||
node_id
|
|
||||||
}
|
|
||||||
TemplateNode::DynamicText { .. } => {
|
|
||||||
let node_id = self.create_node(Node::new(NodeType::Text {
|
|
||||||
text: String::new(),
|
|
||||||
}));
|
|
||||||
node_id
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,7 +159,7 @@ impl<S: State> RealDom<S> {
|
|||||||
for template in mutations.templates {
|
for template in mutations.templates {
|
||||||
let mut template_root_ids = Vec::new();
|
let mut template_root_ids = Vec::new();
|
||||||
for root in template.roots {
|
for root in template.roots {
|
||||||
let id = self.create_template_node(root, &mut nodes_updated);
|
let id = self.create_template_node(root);
|
||||||
template_root_ids.push(id);
|
template_root_ids.push(id);
|
||||||
}
|
}
|
||||||
self.templates
|
self.templates
|
||||||
|
|||||||
@@ -0,0 +1,484 @@
|
|||||||
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
|
use dioxus_html::input_data::keyboard_types::{Code, Key, Modifiers};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Pos {
|
||||||
|
pub col: usize,
|
||||||
|
pub row: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pos {
|
||||||
|
pub fn new(col: usize, row: usize) -> Self {
|
||||||
|
Self { row, col }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn up(&mut self, rope: &str) {
|
||||||
|
self.move_row(-1, rope);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn down(&mut self, rope: &str) {
|
||||||
|
self.move_row(1, rope);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn right(&mut self, rope: &str) {
|
||||||
|
self.move_col(1, rope);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn left(&mut self, rope: &str) {
|
||||||
|
self.move_col(-1, rope);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_row(&mut self, change: i32, rope: &str) {
|
||||||
|
let new = self.row as i32 + change;
|
||||||
|
if new >= 0 && new < rope.lines().count() as i32 {
|
||||||
|
self.row = new as usize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_col(&mut self, change: i32, rope: &str) {
|
||||||
|
self.realize_col(rope);
|
||||||
|
let idx = self.idx(rope) as i32;
|
||||||
|
if idx + change >= 0 && idx + change <= rope.len() as i32 {
|
||||||
|
let len_line = self.len_line(rope) as i32;
|
||||||
|
let new_col = self.col as i32 + change;
|
||||||
|
let diff = new_col - len_line;
|
||||||
|
if diff > 0 {
|
||||||
|
self.down(rope);
|
||||||
|
self.col = 0;
|
||||||
|
self.move_col(diff - 1, rope);
|
||||||
|
} else if new_col < 0 {
|
||||||
|
self.up(rope);
|
||||||
|
self.col = self.len_line(rope);
|
||||||
|
self.move_col(new_col + 1, rope);
|
||||||
|
} else {
|
||||||
|
self.col = new_col as usize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn col(&self, rope: &str) -> usize {
|
||||||
|
self.col.min(self.len_line(rope))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn row(&self) -> usize {
|
||||||
|
self.row
|
||||||
|
}
|
||||||
|
|
||||||
|
fn len_line(&self, rope: &str) -> usize {
|
||||||
|
let line = rope.lines().nth(self.row).unwrap_or_default();
|
||||||
|
let len = line.len();
|
||||||
|
if len > 0 && line.chars().nth(len - 1) == Some('\n') {
|
||||||
|
len - 1
|
||||||
|
} else {
|
||||||
|
len
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn idx(&self, rope: &str) -> usize {
|
||||||
|
rope.lines().take(self.row).map(|l| l.len()).sum::<usize>() + self.col(rope)
|
||||||
|
}
|
||||||
|
|
||||||
|
// the column can be more than the line length, cap it
|
||||||
|
pub fn realize_col(&mut self, rope: &str) {
|
||||||
|
self.col = self.col(rope);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for Pos {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
self.row.cmp(&other.row).then(self.col.cmp(&other.col))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for Pos {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Cursor {
|
||||||
|
pub start: Pos,
|
||||||
|
pub end: Option<Pos>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cursor {
|
||||||
|
pub fn from_start(pos: Pos) -> Self {
|
||||||
|
Self {
|
||||||
|
start: pos,
|
||||||
|
end: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(start: Pos, end: Pos) -> Self {
|
||||||
|
Self {
|
||||||
|
start,
|
||||||
|
end: Some(end),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_cursor(&mut self, f: impl FnOnce(&mut Pos), shift: bool) {
|
||||||
|
if shift {
|
||||||
|
self.with_end(f);
|
||||||
|
} else {
|
||||||
|
f(&mut self.start);
|
||||||
|
self.end = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_selection(&mut self, text: &mut String) -> [i32; 2] {
|
||||||
|
let first = self.first();
|
||||||
|
let last = self.last();
|
||||||
|
let dr = first.row as i32 - last.row as i32;
|
||||||
|
let dc = if dr != 0 {
|
||||||
|
-(last.col as i32)
|
||||||
|
} else {
|
||||||
|
first.col as i32 - last.col as i32
|
||||||
|
};
|
||||||
|
text.replace_range(first.idx(text)..last.idx(text), "");
|
||||||
|
if let Some(end) = self.end.take() {
|
||||||
|
if self.start > end {
|
||||||
|
self.start = end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[dc, dr]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_input(
|
||||||
|
&mut self,
|
||||||
|
data: &dioxus_html::KeyboardData,
|
||||||
|
text: &mut String,
|
||||||
|
max_width: usize,
|
||||||
|
) {
|
||||||
|
use Code::*;
|
||||||
|
match data.code() {
|
||||||
|
ArrowUp => {
|
||||||
|
self.move_cursor(|c| c.up(text), data.modifiers().contains(Modifiers::SHIFT));
|
||||||
|
}
|
||||||
|
ArrowDown => {
|
||||||
|
self.move_cursor(
|
||||||
|
|c| c.down(text),
|
||||||
|
data.modifiers().contains(Modifiers::SHIFT),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ArrowRight => {
|
||||||
|
if data.modifiers().contains(Modifiers::CONTROL) {
|
||||||
|
self.move_cursor(
|
||||||
|
|c| {
|
||||||
|
let mut change = 1;
|
||||||
|
let idx = c.idx(text);
|
||||||
|
let length = text.len();
|
||||||
|
while idx + change < length {
|
||||||
|
let chr = text.chars().nth(idx + change).unwrap();
|
||||||
|
if chr.is_whitespace() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
change += 1;
|
||||||
|
}
|
||||||
|
c.move_col(change as i32, text);
|
||||||
|
},
|
||||||
|
data.modifiers().contains(Modifiers::SHIFT),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
self.move_cursor(
|
||||||
|
|c| c.right(text),
|
||||||
|
data.modifiers().contains(Modifiers::SHIFT),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ArrowLeft => {
|
||||||
|
if data.modifiers().contains(Modifiers::CONTROL) {
|
||||||
|
self.move_cursor(
|
||||||
|
|c| {
|
||||||
|
let mut change = -1;
|
||||||
|
let idx = c.idx(text) as i32;
|
||||||
|
while idx + change > 0 {
|
||||||
|
let chr = text.chars().nth((idx + change) as usize).unwrap();
|
||||||
|
if chr == ' ' {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
change -= 1;
|
||||||
|
}
|
||||||
|
c.move_col(change as i32, text);
|
||||||
|
},
|
||||||
|
data.modifiers().contains(Modifiers::SHIFT),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
self.move_cursor(
|
||||||
|
|c| c.left(text),
|
||||||
|
data.modifiers().contains(Modifiers::SHIFT),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
End => {
|
||||||
|
self.move_cursor(
|
||||||
|
|c| c.col = c.len_line(text),
|
||||||
|
data.modifiers().contains(Modifiers::SHIFT),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Home => {
|
||||||
|
self.move_cursor(|c| c.col = 0, data.modifiers().contains(Modifiers::SHIFT));
|
||||||
|
}
|
||||||
|
Backspace => {
|
||||||
|
self.start.realize_col(text);
|
||||||
|
let mut start_idx = self.start.idx(text);
|
||||||
|
if self.end.is_some() {
|
||||||
|
self.delete_selection(text);
|
||||||
|
} else if start_idx > 0 {
|
||||||
|
self.start.left(text);
|
||||||
|
text.replace_range(start_idx - 1..start_idx, "");
|
||||||
|
if data.modifiers().contains(Modifiers::CONTROL) {
|
||||||
|
start_idx = self.start.idx(text);
|
||||||
|
while start_idx > 0
|
||||||
|
&& text
|
||||||
|
.chars()
|
||||||
|
.nth(start_idx - 1)
|
||||||
|
.filter(|c| *c != ' ')
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
self.start.left(text);
|
||||||
|
text.replace_range(start_idx - 1..start_idx, "");
|
||||||
|
start_idx = self.start.idx(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Enter => {
|
||||||
|
if text.len() + 1 - self.selection_len(text) <= max_width {
|
||||||
|
text.insert(self.start.idx(text), '\n');
|
||||||
|
self.start.col = 0;
|
||||||
|
self.start.down(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Tab => {
|
||||||
|
if text.len() + 1 - self.selection_len(text) <= max_width {
|
||||||
|
self.start.realize_col(text);
|
||||||
|
self.delete_selection(text);
|
||||||
|
text.insert(self.start.idx(text), '\t');
|
||||||
|
self.start.right(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
self.start.realize_col(text);
|
||||||
|
if let Key::Character(character) = data.key() {
|
||||||
|
if text.len() + 1 - self.selection_len(text) <= max_width {
|
||||||
|
self.delete_selection(text);
|
||||||
|
let character = character.chars().next().unwrap();
|
||||||
|
text.insert(self.start.idx(text), character);
|
||||||
|
self.start.right(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_end(&mut self, f: impl FnOnce(&mut Pos)) {
|
||||||
|
let mut new = self.end.take().unwrap_or_else(|| self.start.clone());
|
||||||
|
f(&mut new);
|
||||||
|
self.end.replace(new);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn first(&self) -> &Pos {
|
||||||
|
if let Some(e) = &self.end {
|
||||||
|
e.min(&self.start)
|
||||||
|
} else {
|
||||||
|
&self.start
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn last(&self) -> &Pos {
|
||||||
|
if let Some(e) = &self.end {
|
||||||
|
e.max(&self.start)
|
||||||
|
} else {
|
||||||
|
&self.start
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn selection_len(&self, text: &str) -> usize {
|
||||||
|
self.last().idx(text) - self.first().idx(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Cursor {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
start: Pos::new(0, 0),
|
||||||
|
end: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pos_direction_movement() {
|
||||||
|
let mut pos = Pos::new(100, 0);
|
||||||
|
let text = "hello world\nhi";
|
||||||
|
|
||||||
|
assert_eq!(pos.col(text), text.lines().next().unwrap_or_default().len());
|
||||||
|
pos.down(text);
|
||||||
|
assert_eq!(pos.col(text), text.lines().nth(1).unwrap_or_default().len());
|
||||||
|
pos.up(text);
|
||||||
|
assert_eq!(pos.col(text), text.lines().next().unwrap_or_default().len());
|
||||||
|
pos.left(text);
|
||||||
|
assert_eq!(
|
||||||
|
pos.col(text),
|
||||||
|
text.lines().next().unwrap_or_default().len() - 1
|
||||||
|
);
|
||||||
|
pos.right(text);
|
||||||
|
assert_eq!(pos.col(text), text.lines().next().unwrap_or_default().len());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pos_col_movement() {
|
||||||
|
let mut pos = Pos::new(100, 0);
|
||||||
|
let text = "hello world\nhi";
|
||||||
|
|
||||||
|
// move inside a row
|
||||||
|
pos.move_col(-5, text);
|
||||||
|
assert_eq!(
|
||||||
|
pos.col(text),
|
||||||
|
text.lines().next().unwrap_or_default().len() - 5
|
||||||
|
);
|
||||||
|
pos.move_col(5, text);
|
||||||
|
assert_eq!(pos.col(text), text.lines().next().unwrap_or_default().len());
|
||||||
|
|
||||||
|
// move between rows
|
||||||
|
pos.move_col(3, text);
|
||||||
|
assert_eq!(pos.col(text), 2);
|
||||||
|
pos.move_col(-3, text);
|
||||||
|
assert_eq!(pos.col(text), text.lines().next().unwrap_or_default().len());
|
||||||
|
|
||||||
|
// don't panic if moving out of range
|
||||||
|
pos.move_col(-100, text);
|
||||||
|
pos.move_col(1000, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cursor_row_movement() {
|
||||||
|
let mut pos = Pos::new(100, 0);
|
||||||
|
let text = "hello world\nhi";
|
||||||
|
|
||||||
|
pos.move_row(1, text);
|
||||||
|
assert_eq!(pos.row(), 1);
|
||||||
|
pos.move_row(-1, text);
|
||||||
|
assert_eq!(pos.row(), 0);
|
||||||
|
|
||||||
|
// don't panic if moving out of range
|
||||||
|
pos.move_row(-100, text);
|
||||||
|
pos.move_row(1000, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cursor_input() {
|
||||||
|
let mut cursor = Cursor::from_start(Pos::new(0, 0));
|
||||||
|
let mut text = "hello world\nhi".to_string();
|
||||||
|
|
||||||
|
for _ in 0..5 {
|
||||||
|
cursor.handle_input(
|
||||||
|
&dioxus_html::KeyboardData::new(
|
||||||
|
dioxus_html::input_data::keyboard_types::Key::ArrowRight,
|
||||||
|
dioxus_html::input_data::keyboard_types::Code::ArrowRight,
|
||||||
|
dioxus_html::input_data::keyboard_types::Location::Standard,
|
||||||
|
false,
|
||||||
|
Modifiers::empty(),
|
||||||
|
),
|
||||||
|
&mut text,
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for _ in 0..5 {
|
||||||
|
cursor.handle_input(
|
||||||
|
&dioxus_html::KeyboardData::new(
|
||||||
|
dioxus_html::input_data::keyboard_types::Key::Backspace,
|
||||||
|
dioxus_html::input_data::keyboard_types::Code::Backspace,
|
||||||
|
dioxus_html::input_data::keyboard_types::Location::Standard,
|
||||||
|
false,
|
||||||
|
Modifiers::empty(),
|
||||||
|
),
|
||||||
|
&mut text,
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(text, " world\nhi");
|
||||||
|
|
||||||
|
let goal_text = "hello world\nhi";
|
||||||
|
let max_width = goal_text.len();
|
||||||
|
cursor.handle_input(
|
||||||
|
&dioxus_html::KeyboardData::new(
|
||||||
|
dioxus_html::input_data::keyboard_types::Key::Character("h".to_string()),
|
||||||
|
dioxus_html::input_data::keyboard_types::Code::KeyH,
|
||||||
|
dioxus_html::input_data::keyboard_types::Location::Standard,
|
||||||
|
false,
|
||||||
|
Modifiers::empty(),
|
||||||
|
),
|
||||||
|
&mut text,
|
||||||
|
max_width,
|
||||||
|
);
|
||||||
|
|
||||||
|
cursor.handle_input(
|
||||||
|
&dioxus_html::KeyboardData::new(
|
||||||
|
dioxus_html::input_data::keyboard_types::Key::Character("e".to_string()),
|
||||||
|
dioxus_html::input_data::keyboard_types::Code::KeyE,
|
||||||
|
dioxus_html::input_data::keyboard_types::Location::Standard,
|
||||||
|
false,
|
||||||
|
Modifiers::empty(),
|
||||||
|
),
|
||||||
|
&mut text,
|
||||||
|
max_width,
|
||||||
|
);
|
||||||
|
|
||||||
|
cursor.handle_input(
|
||||||
|
&dioxus_html::KeyboardData::new(
|
||||||
|
dioxus_html::input_data::keyboard_types::Key::Character("l".to_string()),
|
||||||
|
dioxus_html::input_data::keyboard_types::Code::KeyL,
|
||||||
|
dioxus_html::input_data::keyboard_types::Location::Standard,
|
||||||
|
false,
|
||||||
|
Modifiers::empty(),
|
||||||
|
),
|
||||||
|
&mut text,
|
||||||
|
max_width,
|
||||||
|
);
|
||||||
|
|
||||||
|
cursor.handle_input(
|
||||||
|
&dioxus_html::KeyboardData::new(
|
||||||
|
dioxus_html::input_data::keyboard_types::Key::Character("l".to_string()),
|
||||||
|
dioxus_html::input_data::keyboard_types::Code::KeyL,
|
||||||
|
dioxus_html::input_data::keyboard_types::Location::Standard,
|
||||||
|
false,
|
||||||
|
Modifiers::empty(),
|
||||||
|
),
|
||||||
|
&mut text,
|
||||||
|
max_width,
|
||||||
|
);
|
||||||
|
|
||||||
|
cursor.handle_input(
|
||||||
|
&dioxus_html::KeyboardData::new(
|
||||||
|
dioxus_html::input_data::keyboard_types::Key::Character("o".to_string()),
|
||||||
|
dioxus_html::input_data::keyboard_types::Code::KeyO,
|
||||||
|
dioxus_html::input_data::keyboard_types::Location::Standard,
|
||||||
|
false,
|
||||||
|
Modifiers::empty(),
|
||||||
|
),
|
||||||
|
&mut text,
|
||||||
|
max_width,
|
||||||
|
);
|
||||||
|
|
||||||
|
// these should be ignored
|
||||||
|
for _ in 0..10 {
|
||||||
|
cursor.handle_input(
|
||||||
|
&dioxus_html::KeyboardData::new(
|
||||||
|
dioxus_html::input_data::keyboard_types::Key::Character("o".to_string()),
|
||||||
|
dioxus_html::input_data::keyboard_types::Code::KeyO,
|
||||||
|
dioxus_html::input_data::keyboard_types::Location::Standard,
|
||||||
|
false,
|
||||||
|
Modifiers::empty(),
|
||||||
|
),
|
||||||
|
&mut text,
|
||||||
|
max_width,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(text.to_string(), goal_text);
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
mod persistant_iterator;
|
||||||
|
pub use persistant_iterator::*;
|
||||||
|
pub mod cursor;
|
||||||
File renamed without changes.
@@ -1,8 +1,13 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "dioxus-rsx"
|
name = "dioxus-rsx"
|
||||||
version = "0.0.0"
|
version = "0.0.1"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "MIT/Apache-2.0"
|
license = "MIT/Apache-2.0"
|
||||||
|
description = "Core functionality for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences"
|
||||||
|
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||||
|
homepage = "https://dioxuslabs.com"
|
||||||
|
documentation = "https://docs.rs/dioxus-rsx"
|
||||||
|
keywords = ["dom", "ui", "gui", "react", "wasm"]
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
@@ -10,5 +15,4 @@ license = "MIT/Apache-2.0"
|
|||||||
proc-macro2 = { version = "1.0", features = ["span-locations"] }
|
proc-macro2 = { version = "1.0", features = ["span-locations"] }
|
||||||
syn = { version = "1.0", features = ["full", "extra-traits"] }
|
syn = { version = "1.0", features = ["full", "extra-traits"] }
|
||||||
quote = { version = "1.0" }
|
quote = { version = "1.0" }
|
||||||
dioxus-core = { path = "../core", features = ["serialize"] }
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
@@ -45,7 +45,7 @@ impl Parse for Element {
|
|||||||
|
|
||||||
content.parse::<Token![:]>()?;
|
content.parse::<Token![:]>()?;
|
||||||
|
|
||||||
if content.peek(LitStr) && content.peek2(Token![,]) {
|
if content.peek(LitStr) {
|
||||||
let value = content.parse()?;
|
let value = content.parse()?;
|
||||||
attributes.push(ElementAttrNamed {
|
attributes.push(ElementAttrNamed {
|
||||||
el_name: el_name.clone(),
|
el_name: el_name.clone(),
|
||||||
@@ -53,7 +53,6 @@ impl Parse for Element {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
let value = content.parse::<Expr>()?;
|
let value = content.parse::<Expr>()?;
|
||||||
|
|
||||||
attributes.push(ElementAttrNamed {
|
attributes.push(ElementAttrNamed {
|
||||||
el_name: el_name.clone(),
|
el_name: el_name.clone(),
|
||||||
attr: ElementAttr::CustomAttrExpression { name, value },
|
attr: ElementAttr::CustomAttrExpression { name, value },
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ impl<'a> DynamicContext<'a> {
|
|||||||
|
|
||||||
let static_attrs = el.attributes.iter().map(|attr| match &attr.attr {
|
let static_attrs = el.attributes.iter().map(|attr| match &attr.attr {
|
||||||
ElementAttr::AttrText { name, value } if value.is_static() => {
|
ElementAttr::AttrText { name, value } if value.is_static() => {
|
||||||
let value = value.source.as_ref().unwrap();
|
let value = value.to_static().unwrap();
|
||||||
quote! {
|
quote! {
|
||||||
::dioxus::core::TemplateAttribute::Static {
|
::dioxus::core::TemplateAttribute::Static {
|
||||||
name: dioxus_elements::#el_name::#name.0,
|
name: dioxus_elements::#el_name::#name.0,
|
||||||
@@ -196,7 +196,7 @@ impl<'a> DynamicContext<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ElementAttr::CustomAttrText { name, value } if value.is_static() => {
|
ElementAttr::CustomAttrText { name, value } if value.is_static() => {
|
||||||
let value = value.source.as_ref().unwrap();
|
let value = value.to_static().unwrap();
|
||||||
quote! {
|
quote! {
|
||||||
::dioxus::core::TemplateAttribute::Static {
|
::dioxus::core::TemplateAttribute::Static {
|
||||||
name: #name,
|
name: #name,
|
||||||
@@ -244,7 +244,7 @@ impl<'a> DynamicContext<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
BodyNode::Text(text) if text.is_static() => {
|
BodyNode::Text(text) if text.is_static() => {
|
||||||
let text = text.source.as_ref().unwrap();
|
let text = text.to_static().unwrap();
|
||||||
quote! { ::dioxus::core::TemplateNode::Text{ text: #text } }
|
quote! { ::dioxus::core::TemplateNode::Text{ text: #text } }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ license = "MIT/Apache-2.0"
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
dioxus = { path = "../dioxus", version = "^0.2.1" }
|
||||||
dioxus-core = { path = "../core", version = "^0.2.1" }
|
dioxus-core = { path = "../core", version = "^0.2.1" }
|
||||||
dioxus-html = { path = "../html", version = "^0.2.1" }
|
dioxus-html = { path = "../html", version = "^0.2.1" }
|
||||||
dioxus-native-core = { path = "../native-core", version = "^0.2.0" }
|
dioxus-native-core = { path = "../native-core", version = "^0.2.0" }
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_html::FormData;
|
||||||
|
use dioxus_tui::prelude::*;
|
||||||
|
use dioxus_tui::Config;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
dioxus_tui::launch_cfg(app, Config::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
let bg_green = use_state(cx, || false);
|
||||||
|
|
||||||
|
let color = if *bg_green.get() { "green" } else { "red" };
|
||||||
|
cx.render(rsx! {
|
||||||
|
div{
|
||||||
|
width: "100%",
|
||||||
|
background_color: "{color}",
|
||||||
|
flex_direction: "column",
|
||||||
|
align_items: "center",
|
||||||
|
justify_content: "center",
|
||||||
|
|
||||||
|
Input{
|
||||||
|
oninput: |data: FormData| if &data.value == "good"{
|
||||||
|
bg_green.set(true);
|
||||||
|
} else{
|
||||||
|
bg_green.set(false);
|
||||||
|
},
|
||||||
|
r#type: "checkbox",
|
||||||
|
value: "good",
|
||||||
|
width: "50%",
|
||||||
|
height: "10%",
|
||||||
|
checked: "true",
|
||||||
|
}
|
||||||
|
Input{
|
||||||
|
oninput: |data: FormData| if &data.value == "hello world"{
|
||||||
|
bg_green.set(true);
|
||||||
|
} else{
|
||||||
|
bg_green.set(false);
|
||||||
|
},
|
||||||
|
width: "50%",
|
||||||
|
height: "10%",
|
||||||
|
maxlength: "11",
|
||||||
|
}
|
||||||
|
Input{
|
||||||
|
oninput: |data: FormData| {
|
||||||
|
if (data.value.parse::<f32>().unwrap() - 40.0).abs() < 5.0 {
|
||||||
|
bg_green.set(true);
|
||||||
|
} else{
|
||||||
|
bg_green.set(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
r#type: "range",
|
||||||
|
width: "50%",
|
||||||
|
height: "10%",
|
||||||
|
min: "20",
|
||||||
|
max: "80",
|
||||||
|
}
|
||||||
|
Input{
|
||||||
|
oninput: |data: FormData| {
|
||||||
|
if data.value == "10"{
|
||||||
|
bg_green.set(true);
|
||||||
|
} else{
|
||||||
|
bg_green.set(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
r#type: "number",
|
||||||
|
width: "50%",
|
||||||
|
height: "10%",
|
||||||
|
maxlength: "4",
|
||||||
|
}
|
||||||
|
Input{
|
||||||
|
oninput: |data: FormData| {
|
||||||
|
if data.value == "hello world"{
|
||||||
|
bg_green.set(true);
|
||||||
|
} else{
|
||||||
|
bg_green.set(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
r#type: "password",
|
||||||
|
width: "50%",
|
||||||
|
height: "10%",
|
||||||
|
maxlength: "11",
|
||||||
|
}
|
||||||
|
Input{
|
||||||
|
onclick: |_: FormData| bg_green.set(true),
|
||||||
|
r#type: "button",
|
||||||
|
value: "green",
|
||||||
|
width: "50%",
|
||||||
|
height: "10%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -287,7 +287,10 @@ impl InnerInputState {
|
|||||||
|
|
||||||
fn prepare_mouse_data(mouse_data: &MouseData, layout: &Layout) -> MouseData {
|
fn prepare_mouse_data(mouse_data: &MouseData, layout: &Layout) -> MouseData {
|
||||||
let Point { x, y } = layout.location;
|
let Point { x, y } = layout.location;
|
||||||
let node_origin = ClientPoint::new(x.into(), y.into());
|
let node_origin = ClientPoint::new(
|
||||||
|
layout_to_screen_space(x).into(),
|
||||||
|
layout_to_screen_space(y).into(),
|
||||||
|
);
|
||||||
|
|
||||||
let new_client_coordinates = (mouse_data.client_coordinates() - node_origin)
|
let new_client_coordinates = (mouse_data.client_coordinates() - node_origin)
|
||||||
.to_point()
|
.to_point()
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use dioxus_native_core::state::ChildDepState;
|
|||||||
use dioxus_native_core_macro::sorted_str_slice;
|
use dioxus_native_core_macro::sorted_str_slice;
|
||||||
use taffy::prelude::*;
|
use taffy::prelude::*;
|
||||||
|
|
||||||
use crate::screen_to_layout_space;
|
use crate::{screen_to_layout_space, unit_to_layout_space};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub(crate) enum PossiblyUninitalized<T> {
|
pub(crate) enum PossiblyUninitalized<T> {
|
||||||
@@ -105,6 +105,59 @@ impl ChildDepState for TaffyLayout {
|
|||||||
child_layout.push(l.node.unwrap());
|
child_layout.push(l.node.unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn scale_dimention(d: Dimension) -> Dimension {
|
||||||
|
match d {
|
||||||
|
Dimension::Points(p) => Dimension::Points(unit_to_layout_space(p)),
|
||||||
|
Dimension::Percent(p) => Dimension::Percent(p),
|
||||||
|
Dimension::Auto => Dimension::Auto,
|
||||||
|
Dimension::Undefined => Dimension::Undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let style = Style {
|
||||||
|
position: Rect {
|
||||||
|
left: scale_dimention(style.position.left),
|
||||||
|
right: scale_dimention(style.position.right),
|
||||||
|
top: scale_dimention(style.position.top),
|
||||||
|
bottom: scale_dimention(style.position.bottom),
|
||||||
|
},
|
||||||
|
margin: Rect {
|
||||||
|
left: scale_dimention(style.margin.left),
|
||||||
|
right: scale_dimention(style.margin.right),
|
||||||
|
top: scale_dimention(style.margin.top),
|
||||||
|
bottom: scale_dimention(style.margin.bottom),
|
||||||
|
},
|
||||||
|
padding: Rect {
|
||||||
|
left: scale_dimention(style.padding.left),
|
||||||
|
right: scale_dimention(style.padding.right),
|
||||||
|
top: scale_dimention(style.padding.top),
|
||||||
|
bottom: scale_dimention(style.padding.bottom),
|
||||||
|
},
|
||||||
|
border: Rect {
|
||||||
|
left: scale_dimention(style.border.left),
|
||||||
|
right: scale_dimention(style.border.right),
|
||||||
|
top: scale_dimention(style.border.top),
|
||||||
|
bottom: scale_dimention(style.border.bottom),
|
||||||
|
},
|
||||||
|
gap: Size {
|
||||||
|
width: scale_dimention(style.gap.width),
|
||||||
|
height: scale_dimention(style.gap.height),
|
||||||
|
},
|
||||||
|
flex_basis: scale_dimention(style.flex_basis),
|
||||||
|
size: Size {
|
||||||
|
width: scale_dimention(style.size.width),
|
||||||
|
height: scale_dimention(style.size.height),
|
||||||
|
},
|
||||||
|
min_size: Size {
|
||||||
|
width: scale_dimention(style.min_size.width),
|
||||||
|
height: scale_dimention(style.min_size.height),
|
||||||
|
},
|
||||||
|
max_size: Size {
|
||||||
|
width: scale_dimention(style.max_size.width),
|
||||||
|
height: scale_dimention(style.max_size.height),
|
||||||
|
},
|
||||||
|
..style
|
||||||
|
};
|
||||||
|
|
||||||
if let PossiblyUninitalized::Initialized(n) = self.node {
|
if let PossiblyUninitalized::Initialized(n) = self.node {
|
||||||
if self.style != style {
|
if self.style != style {
|
||||||
taffy.set_style(n, style).unwrap();
|
taffy.set_style(n, style).unwrap();
|
||||||
|
|||||||
+18
-3
@@ -1,5 +1,6 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
|
cursor::{MoveTo, RestorePosition, SavePosition, Show},
|
||||||
event::{DisableMouseCapture, EnableMouseCapture, Event as TermEvent, KeyCode, KeyModifiers},
|
event::{DisableMouseCapture, EnableMouseCapture, Event as TermEvent, KeyCode, KeyModifiers},
|
||||||
execute,
|
execute,
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
@@ -28,11 +29,13 @@ mod focus;
|
|||||||
mod hooks;
|
mod hooks;
|
||||||
mod layout;
|
mod layout;
|
||||||
mod node;
|
mod node;
|
||||||
|
pub mod prelude;
|
||||||
pub mod query;
|
pub mod query;
|
||||||
mod render;
|
mod render;
|
||||||
mod style;
|
mod style;
|
||||||
mod style_attributes;
|
mod style_attributes;
|
||||||
mod widget;
|
mod widget;
|
||||||
|
mod widgets;
|
||||||
|
|
||||||
pub use config::*;
|
pub use config::*;
|
||||||
pub use hooks::*;
|
pub use hooks::*;
|
||||||
@@ -43,6 +46,10 @@ pub(crate) fn screen_to_layout_space(screen: u16) -> f32 {
|
|||||||
screen as f32 * 10.0
|
screen as f32 * 10.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn unit_to_layout_space(screen: f32) -> f32 {
|
||||||
|
screen * 10.0
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn layout_to_screen_space(layout: f32) -> f32 {
|
pub(crate) fn layout_to_screen_space(layout: f32) -> f32 {
|
||||||
layout / 10.0
|
layout / 10.0
|
||||||
}
|
}
|
||||||
@@ -136,7 +143,13 @@ fn render_vdom(
|
|||||||
let mut terminal = (!cfg.headless).then(|| {
|
let mut terminal = (!cfg.headless).then(|| {
|
||||||
enable_raw_mode().unwrap();
|
enable_raw_mode().unwrap();
|
||||||
let mut stdout = std::io::stdout();
|
let mut stdout = std::io::stdout();
|
||||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture).unwrap();
|
execute!(
|
||||||
|
stdout,
|
||||||
|
EnterAlternateScreen,
|
||||||
|
EnableMouseCapture,
|
||||||
|
MoveTo(0, 1000)
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
let backend = CrosstermBackend::new(io::stdout());
|
let backend = CrosstermBackend::new(io::stdout());
|
||||||
Terminal::new(backend).unwrap()
|
Terminal::new(backend).unwrap()
|
||||||
});
|
});
|
||||||
@@ -181,14 +194,16 @@ fn render_vdom(
|
|||||||
taffy.compute_layout(root_node, size).unwrap();
|
taffy.compute_layout(root_node, size).unwrap();
|
||||||
}
|
}
|
||||||
if let Some(terminal) = &mut terminal {
|
if let Some(terminal) = &mut terminal {
|
||||||
|
execute!(terminal.backend_mut(), SavePosition).unwrap();
|
||||||
terminal.draw(|frame| {
|
terminal.draw(|frame| {
|
||||||
let rdom = rdom.borrow();
|
let rdom = rdom.borrow();
|
||||||
let mut taffy = taffy.lock().expect("taffy lock poisoned");
|
let mut taffy = taffy.lock().expect("taffy lock poisoned");
|
||||||
// size is guaranteed to not change when rendering
|
// size is guaranteed to not change when rendering
|
||||||
resize(frame.size(), &mut *taffy, &rdom);
|
resize(frame.size(), &mut taffy, &rdom);
|
||||||
let root = &rdom[NodeId(0)];
|
let root = &rdom[NodeId(0)];
|
||||||
render::render_vnode(frame, &*taffy, &rdom, root, cfg, Point::ZERO);
|
render::render_vnode(frame, &taffy, &rdom, root, cfg, Point::ZERO);
|
||||||
})?;
|
})?;
|
||||||
|
execute!(terminal.backend_mut(), RestorePosition, Show).unwrap();
|
||||||
} else {
|
} else {
|
||||||
let rdom = rdom.borrow();
|
let rdom = rdom.borrow();
|
||||||
resize(
|
resize(
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
pub use crate::widgets::*;
|
||||||
@@ -89,7 +89,7 @@ impl<'a> ElementRef<'a> {
|
|||||||
.ok();
|
.ok();
|
||||||
layout.map(|layout| Layout {
|
layout.map(|layout| Layout {
|
||||||
order: layout.order,
|
order: layout.order,
|
||||||
size: layout.size.map(|v| layout_to_screen_space(v)),
|
size: layout.size.map(layout_to_screen_space),
|
||||||
location: Point {
|
location: Point {
|
||||||
x: layout_to_screen_space(layout.location.x),
|
x: layout_to_screen_space(layout.location.x),
|
||||||
y: layout_to_screen_space(layout.location.y),
|
y: layout_to_screen_space(layout.location.y),
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ pub(crate) fn render_vnode(
|
|||||||
let x = layout_to_screen_space(fx).round() as u16;
|
let x = layout_to_screen_space(fx).round() as u16;
|
||||||
let y = layout_to_screen_space(fy).round() as u16;
|
let y = layout_to_screen_space(fy).round() as u16;
|
||||||
let Size { width, height } = *size;
|
let Size { width, height } = *size;
|
||||||
let width = layout_to_screen_space(fx + width).round() as u16 + x;
|
let width = layout_to_screen_space(fx + width).round() as u16 - x;
|
||||||
let height = layout_to_screen_space(fy + height).round() as u16 - y;
|
let height = layout_to_screen_space(fy + height).round() as u16 - y;
|
||||||
|
|
||||||
match &node.node_data.node_type {
|
match &node.node_data.node_type {
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_elements::input_data::keyboard_types::Key;
|
||||||
|
use dioxus_html as dioxus_elements;
|
||||||
|
use dioxus_html::FormData;
|
||||||
|
|
||||||
|
#[derive(Props)]
|
||||||
|
pub(crate) struct ButtonProps<'a> {
|
||||||
|
#[props(!optional)]
|
||||||
|
raw_onclick: Option<&'a EventHandler<'a, FormData>>,
|
||||||
|
#[props(!optional)]
|
||||||
|
value: Option<&'a str>,
|
||||||
|
#[props(!optional)]
|
||||||
|
width: Option<&'a str>,
|
||||||
|
#[props(!optional)]
|
||||||
|
height: Option<&'a str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub(crate) fn Button<'a>(cx: Scope<'a, ButtonProps>) -> Element<'a> {
|
||||||
|
let state = use_state(cx, || false);
|
||||||
|
let width = cx.props.width.unwrap_or("1px");
|
||||||
|
let height = cx.props.height.unwrap_or("1px");
|
||||||
|
|
||||||
|
let single_char = width == "1px" || height == "1px";
|
||||||
|
let text = if let Some(v) = cx.props.value { v } else { "" };
|
||||||
|
let border_style = if single_char { "none" } else { "solid" };
|
||||||
|
let update = || {
|
||||||
|
let new_state = !state.get();
|
||||||
|
if let Some(callback) = cx.props.raw_onclick {
|
||||||
|
callback.call(FormData {
|
||||||
|
value: text.to_string(),
|
||||||
|
values: HashMap::new(),
|
||||||
|
files: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
state.set(new_state);
|
||||||
|
};
|
||||||
|
cx.render(rsx! {
|
||||||
|
div{
|
||||||
|
width: "{width}",
|
||||||
|
height: "{height}",
|
||||||
|
border_style: "{border_style}",
|
||||||
|
flex_direction: "row",
|
||||||
|
align_items: "center",
|
||||||
|
justify_content: "center",
|
||||||
|
onclick: move |_| {
|
||||||
|
update();
|
||||||
|
},
|
||||||
|
onkeydown: move |evt|{
|
||||||
|
if !evt.is_auto_repeating() && match evt.key(){ Key::Character(c) if c == " " =>true, Key::Enter=>true, _=>false } {
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"{text}"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_elements::input_data::keyboard_types::Key;
|
||||||
|
use dioxus_html as dioxus_elements;
|
||||||
|
use dioxus_html::FormData;
|
||||||
|
|
||||||
|
#[derive(Props)]
|
||||||
|
pub(crate) struct CheckBoxProps<'a> {
|
||||||
|
#[props(!optional)]
|
||||||
|
raw_oninput: Option<&'a EventHandler<'a, FormData>>,
|
||||||
|
#[props(!optional)]
|
||||||
|
value: Option<&'a str>,
|
||||||
|
#[props(!optional)]
|
||||||
|
width: Option<&'a str>,
|
||||||
|
#[props(!optional)]
|
||||||
|
height: Option<&'a str>,
|
||||||
|
#[props(!optional)]
|
||||||
|
checked: Option<&'a str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub(crate) fn CheckBox<'a>(cx: Scope<'a, CheckBoxProps>) -> Element<'a> {
|
||||||
|
let state = use_state(cx, || cx.props.checked.filter(|&c| c == "true").is_some());
|
||||||
|
let width = cx.props.width.unwrap_or("1px");
|
||||||
|
let height = cx.props.height.unwrap_or("1px");
|
||||||
|
|
||||||
|
let single_char = width == "1px" && height == "1px";
|
||||||
|
let text = if single_char {
|
||||||
|
if *state.get() {
|
||||||
|
"☑"
|
||||||
|
} else {
|
||||||
|
"☐"
|
||||||
|
}
|
||||||
|
} else if *state.get() {
|
||||||
|
"✓"
|
||||||
|
} else {
|
||||||
|
" "
|
||||||
|
};
|
||||||
|
let border_style = if width == "1px" || height == "1px" {
|
||||||
|
"none"
|
||||||
|
} else {
|
||||||
|
"solid"
|
||||||
|
};
|
||||||
|
let update = move || {
|
||||||
|
let new_state = !state.get();
|
||||||
|
if let Some(callback) = cx.props.raw_oninput {
|
||||||
|
callback.call(FormData {
|
||||||
|
value: if let Some(value) = &cx.props.value {
|
||||||
|
if new_state {
|
||||||
|
value.to_string()
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
"on".to_string()
|
||||||
|
},
|
||||||
|
values: HashMap::new(),
|
||||||
|
files: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
state.set(new_state);
|
||||||
|
};
|
||||||
|
cx.render(rsx! {
|
||||||
|
div {
|
||||||
|
width: "{width}",
|
||||||
|
height: "{height}",
|
||||||
|
border_style: "{border_style}",
|
||||||
|
align_items: "center",
|
||||||
|
justify_content: "center",
|
||||||
|
onclick: move |_| {
|
||||||
|
update();
|
||||||
|
},
|
||||||
|
onkeydown: move |evt| {
|
||||||
|
if !evt.is_auto_repeating() && match evt.key(){ Key::Character(c) if c == " " =>true, Key::Enter=>true, _=>false } {
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"{text}"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_core::prelude::fc_to_builder;
|
||||||
|
use dioxus_html::FormData;
|
||||||
|
|
||||||
|
use crate::widgets::button::Button;
|
||||||
|
use crate::widgets::checkbox::CheckBox;
|
||||||
|
use crate::widgets::number::NumbericInput;
|
||||||
|
use crate::widgets::password::Password;
|
||||||
|
use crate::widgets::slider::Slider;
|
||||||
|
use crate::widgets::textbox::TextBox;
|
||||||
|
|
||||||
|
#[derive(Props)]
|
||||||
|
pub struct InputProps<'a> {
|
||||||
|
r#type: Option<&'static str>,
|
||||||
|
oninput: Option<EventHandler<'a, FormData>>,
|
||||||
|
onclick: Option<EventHandler<'a, FormData>>,
|
||||||
|
value: Option<&'a str>,
|
||||||
|
size: Option<&'a str>,
|
||||||
|
maxlength: Option<&'a str>,
|
||||||
|
width: Option<&'a str>,
|
||||||
|
height: Option<&'a str>,
|
||||||
|
min: Option<&'a str>,
|
||||||
|
max: Option<&'a str>,
|
||||||
|
step: Option<&'a str>,
|
||||||
|
checked: Option<&'a str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Input<'a>(cx: Scope<'a, InputProps<'a>>) -> Element<'a> {
|
||||||
|
cx.render(match cx.props.r#type {
|
||||||
|
Some("checkbox") => {
|
||||||
|
rsx! {
|
||||||
|
CheckBox{
|
||||||
|
raw_oninput: cx.props.oninput.as_ref(),
|
||||||
|
value: cx.props.value,
|
||||||
|
width: cx.props.width,
|
||||||
|
height: cx.props.height,
|
||||||
|
checked: cx.props.checked,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some("range") => {
|
||||||
|
rsx! {
|
||||||
|
Slider{
|
||||||
|
raw_oninput: cx.props.oninput.as_ref(),
|
||||||
|
value: cx.props.value,
|
||||||
|
width: cx.props.width,
|
||||||
|
height: cx.props.height,
|
||||||
|
max: cx.props.max,
|
||||||
|
min: cx.props.min,
|
||||||
|
step: cx.props.step,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some("button") => {
|
||||||
|
rsx! {
|
||||||
|
Button{
|
||||||
|
raw_onclick: cx.props.onclick.as_ref(),
|
||||||
|
value: cx.props.value,
|
||||||
|
width: cx.props.width,
|
||||||
|
height: cx.props.height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some("number") => {
|
||||||
|
rsx! {
|
||||||
|
NumbericInput{
|
||||||
|
raw_oninput: cx.props.oninput.as_ref(),
|
||||||
|
value: cx.props.value,
|
||||||
|
size: cx.props.size,
|
||||||
|
max_length: cx.props.maxlength,
|
||||||
|
width: cx.props.width,
|
||||||
|
height: cx.props.height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some("password") => {
|
||||||
|
rsx! {
|
||||||
|
Password{
|
||||||
|
raw_oninput: cx.props.oninput.as_ref(),
|
||||||
|
value: cx.props.value,
|
||||||
|
size: cx.props.size,
|
||||||
|
max_length: cx.props.maxlength,
|
||||||
|
width: cx.props.width,
|
||||||
|
height: cx.props.height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
rsx! {
|
||||||
|
TextBox{
|
||||||
|
raw_oninput: cx.props.oninput.as_ref(),
|
||||||
|
value: cx.props.value,
|
||||||
|
size: cx.props.size,
|
||||||
|
max_length: cx.props.maxlength,
|
||||||
|
width: cx.props.width,
|
||||||
|
height: cx.props.height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
mod button;
|
||||||
|
mod checkbox;
|
||||||
|
mod input;
|
||||||
|
mod number;
|
||||||
|
mod password;
|
||||||
|
mod slider;
|
||||||
|
mod textbox;
|
||||||
|
|
||||||
|
use dioxus_core::{ElementId, RenderReturn, Scope};
|
||||||
|
pub use input::*;
|
||||||
|
|
||||||
|
pub(crate) fn get_root_id<T>(cx: Scope<T>) -> Option<ElementId> {
|
||||||
|
if let RenderReturn::Sync(Ok(sync)) = cx.root_node() {
|
||||||
|
sync.root_ids.get(0).map(|id| id.get())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
use crate::widgets::get_root_id;
|
||||||
|
use crate::Query;
|
||||||
|
use crossterm::{cursor::MoveTo, execute};
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_elements::input_data::keyboard_types::Key;
|
||||||
|
use dioxus_html as dioxus_elements;
|
||||||
|
use dioxus_html::FormData;
|
||||||
|
use dioxus_native_core::utils::cursor::{Cursor, Pos};
|
||||||
|
use std::{collections::HashMap, io::stdout};
|
||||||
|
use taffy::geometry::Point;
|
||||||
|
|
||||||
|
#[derive(Props)]
|
||||||
|
pub(crate) struct NumbericInputProps<'a> {
|
||||||
|
#[props(!optional)]
|
||||||
|
raw_oninput: Option<&'a EventHandler<'a, FormData>>,
|
||||||
|
#[props(!optional)]
|
||||||
|
value: Option<&'a str>,
|
||||||
|
#[props(!optional)]
|
||||||
|
size: Option<&'a str>,
|
||||||
|
#[props(!optional)]
|
||||||
|
max_length: Option<&'a str>,
|
||||||
|
#[props(!optional)]
|
||||||
|
width: Option<&'a str>,
|
||||||
|
#[props(!optional)]
|
||||||
|
height: Option<&'a str>,
|
||||||
|
}
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub(crate) fn NumbericInput<'a>(cx: Scope<'a, NumbericInputProps>) -> Element<'a> {
|
||||||
|
let tui_query: Query = cx.consume_context().unwrap();
|
||||||
|
let tui_query_clone = tui_query.clone();
|
||||||
|
|
||||||
|
let text_ref = use_ref(cx, || {
|
||||||
|
if let Some(intial_text) = cx.props.value {
|
||||||
|
intial_text.to_string()
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let cursor = use_ref(cx, Cursor::default);
|
||||||
|
let dragging = use_state(cx, || false);
|
||||||
|
|
||||||
|
let text = text_ref.read().clone();
|
||||||
|
let start_highlight = cursor.read().first().idx(&text);
|
||||||
|
let end_highlight = cursor.read().last().idx(&text);
|
||||||
|
let (text_before_first_cursor, text_after_first_cursor) = text.split_at(start_highlight);
|
||||||
|
let (text_highlighted, text_after_second_cursor) =
|
||||||
|
text_after_first_cursor.split_at(end_highlight - start_highlight);
|
||||||
|
|
||||||
|
let max_len = cx
|
||||||
|
.props
|
||||||
|
.max_length
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|s| s.parse().ok())
|
||||||
|
.unwrap_or(usize::MAX);
|
||||||
|
|
||||||
|
let width = cx
|
||||||
|
.props
|
||||||
|
.width
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
// px is the same as em in tui
|
||||||
|
.or_else(|| cx.props.size.map(|s| s.to_string() + "px"))
|
||||||
|
.unwrap_or_else(|| "10px".to_string());
|
||||||
|
let height = cx.props.height.unwrap_or("3px");
|
||||||
|
|
||||||
|
// don't draw a border unless there is enough space
|
||||||
|
let border = if width
|
||||||
|
.strip_suffix("px")
|
||||||
|
.and_then(|w| w.parse::<i32>().ok())
|
||||||
|
.filter(|w| *w < 3)
|
||||||
|
.is_some()
|
||||||
|
|| height
|
||||||
|
.strip_suffix("px")
|
||||||
|
.and_then(|h| h.parse::<i32>().ok())
|
||||||
|
.filter(|h| *h < 3)
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
"none"
|
||||||
|
} else {
|
||||||
|
"solid"
|
||||||
|
};
|
||||||
|
|
||||||
|
let update = |text: String| {
|
||||||
|
if let Some(input_handler) = &cx.props.raw_oninput {
|
||||||
|
input_handler.call(FormData {
|
||||||
|
value: text,
|
||||||
|
values: HashMap::new(),
|
||||||
|
files: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let increase = move || {
|
||||||
|
let mut text = text_ref.write();
|
||||||
|
*text = (text.parse::<f64>().unwrap_or(0.0) + 1.0).to_string();
|
||||||
|
update(text.clone());
|
||||||
|
};
|
||||||
|
let decrease = move || {
|
||||||
|
let mut text = text_ref.write();
|
||||||
|
*text = (text.parse::<f64>().unwrap_or(0.0) - 1.0).to_string();
|
||||||
|
update(text.clone());
|
||||||
|
};
|
||||||
|
|
||||||
|
render! {
|
||||||
|
div{
|
||||||
|
width: "{width}",
|
||||||
|
height: "{height}",
|
||||||
|
border_style: "{border}",
|
||||||
|
|
||||||
|
onkeydown: move |k| {
|
||||||
|
let is_text = match k.key(){
|
||||||
|
Key::ArrowLeft | Key::ArrowRight | Key::Backspace => true,
|
||||||
|
Key::Character(c) if c=="." || c== "-" || c.chars().all(|c|c.is_numeric())=> true,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
if is_text{
|
||||||
|
let mut text = text_ref.write();
|
||||||
|
cursor.write().handle_input(&k, &mut text, max_len);
|
||||||
|
update(text.clone());
|
||||||
|
|
||||||
|
let node = tui_query.get(get_root_id(cx).unwrap());
|
||||||
|
let Point{ x, y } = node.pos().unwrap();
|
||||||
|
|
||||||
|
let Pos { col, row } = cursor.read().start;
|
||||||
|
let (x, y) = (col as u16 + x as u16 + if border == "none" {0} else {1}, row as u16 + y as u16 + if border == "none" {0} else {1});
|
||||||
|
if let Ok(pos) = crossterm::cursor::position() {
|
||||||
|
if pos != (x, y){
|
||||||
|
execute!(stdout(), MoveTo(x, y)).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
execute!(stdout(), MoveTo(x, y)).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
match k.key() {
|
||||||
|
Key::ArrowUp =>{
|
||||||
|
increase();
|
||||||
|
}
|
||||||
|
Key::ArrowDown =>{
|
||||||
|
decrease();
|
||||||
|
}
|
||||||
|
_ => ()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onmousemove: move |evt| {
|
||||||
|
if *dragging.get() {
|
||||||
|
let offset = evt.data.element_coordinates();
|
||||||
|
let mut new = Pos::new(offset.x as usize, offset.y as usize);
|
||||||
|
if border != "none" {
|
||||||
|
new.col = new.col.saturating_sub(1);
|
||||||
|
}
|
||||||
|
// textboxs are only one line tall
|
||||||
|
new.row = 0;
|
||||||
|
|
||||||
|
if new != cursor.read().start {
|
||||||
|
cursor.write().end = Some(new);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onmousedown: move |evt| {
|
||||||
|
let offset = evt.data.element_coordinates();
|
||||||
|
let mut new = Pos::new(offset.x as usize, offset.y as usize);
|
||||||
|
if border != "none" {
|
||||||
|
new.col = new.col.saturating_sub(1);
|
||||||
|
}
|
||||||
|
new.row = 0;
|
||||||
|
|
||||||
|
new.realize_col(&text_ref.read());
|
||||||
|
cursor.set(Cursor::from_start(new));
|
||||||
|
dragging.set(true);
|
||||||
|
let node = tui_query_clone.get(get_root_id(cx).unwrap());
|
||||||
|
let Point{ x, y } = node.pos().unwrap();
|
||||||
|
|
||||||
|
let Pos { col, row } = cursor.read().start;
|
||||||
|
let (x, y) = (col as u16 + x as u16 + if border == "none" {0} else {1}, row as u16 + y as u16 + if border == "none" {0} else {1});
|
||||||
|
if let Ok(pos) = crossterm::cursor::position() {
|
||||||
|
if pos != (x, y){
|
||||||
|
execute!(stdout(), MoveTo(x, y)).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
execute!(stdout(), MoveTo(x, y)).unwrap();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onmouseup: move |_| {
|
||||||
|
dragging.set(false);
|
||||||
|
},
|
||||||
|
onmouseleave: move |_| {
|
||||||
|
dragging.set(false);
|
||||||
|
},
|
||||||
|
onmouseenter: move |_| {
|
||||||
|
dragging.set(false);
|
||||||
|
},
|
||||||
|
onfocusout: |_| {
|
||||||
|
execute!(stdout(), MoveTo(0, 1000)).unwrap();
|
||||||
|
},
|
||||||
|
|
||||||
|
"{text_before_first_cursor}"
|
||||||
|
|
||||||
|
span{
|
||||||
|
background_color: "rgba(255, 255, 255, 50%)",
|
||||||
|
|
||||||
|
"{text_highlighted}"
|
||||||
|
}
|
||||||
|
|
||||||
|
"{text_after_second_cursor}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
use crate::widgets::get_root_id;
|
||||||
|
use crate::Query;
|
||||||
|
use crossterm::{cursor::*, execute};
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_elements::input_data::keyboard_types::Key;
|
||||||
|
use dioxus_html as dioxus_elements;
|
||||||
|
use dioxus_html::FormData;
|
||||||
|
use dioxus_native_core::utils::cursor::{Cursor, Pos};
|
||||||
|
use std::{collections::HashMap, io::stdout};
|
||||||
|
use taffy::geometry::Point;
|
||||||
|
|
||||||
|
#[derive(Props)]
|
||||||
|
pub(crate) struct PasswordProps<'a> {
|
||||||
|
#[props(!optional)]
|
||||||
|
raw_oninput: Option<&'a EventHandler<'a, FormData>>,
|
||||||
|
#[props(!optional)]
|
||||||
|
value: Option<&'a str>,
|
||||||
|
#[props(!optional)]
|
||||||
|
size: Option<&'a str>,
|
||||||
|
#[props(!optional)]
|
||||||
|
max_length: Option<&'a str>,
|
||||||
|
#[props(!optional)]
|
||||||
|
width: Option<&'a str>,
|
||||||
|
#[props(!optional)]
|
||||||
|
height: Option<&'a str>,
|
||||||
|
}
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub(crate) fn Password<'a>(cx: Scope<'a, PasswordProps>) -> Element<'a> {
|
||||||
|
let tui_query: Query = cx.consume_context().unwrap();
|
||||||
|
let tui_query_clone = tui_query.clone();
|
||||||
|
|
||||||
|
let text_ref = use_ref(cx, || {
|
||||||
|
if let Some(intial_text) = cx.props.value {
|
||||||
|
intial_text.to_string()
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let cursor = use_ref(cx, Cursor::default);
|
||||||
|
let dragging = use_state(cx, || false);
|
||||||
|
|
||||||
|
let text = text_ref.read().clone();
|
||||||
|
let start_highlight = cursor.read().first().idx(&text);
|
||||||
|
let end_highlight = cursor.read().last().idx(&text);
|
||||||
|
let (text_before_first_cursor, text_after_first_cursor) = text.split_at(start_highlight);
|
||||||
|
let (text_highlighted, text_after_second_cursor) =
|
||||||
|
text_after_first_cursor.split_at(end_highlight - start_highlight);
|
||||||
|
|
||||||
|
let text_before_first_cursor = ".".repeat(text_before_first_cursor.len());
|
||||||
|
let text_highlighted = ".".repeat(text_highlighted.len());
|
||||||
|
let text_after_second_cursor = ".".repeat(text_after_second_cursor.len());
|
||||||
|
|
||||||
|
let max_len = cx
|
||||||
|
.props
|
||||||
|
.max_length
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|s| s.parse().ok())
|
||||||
|
.unwrap_or(usize::MAX);
|
||||||
|
|
||||||
|
let width = cx
|
||||||
|
.props
|
||||||
|
.width
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
// px is the same as em in tui
|
||||||
|
.or_else(|| cx.props.size.map(|s| s.to_string() + "px"))
|
||||||
|
.unwrap_or_else(|| "10px".to_string());
|
||||||
|
let height = cx.props.height.unwrap_or("3px");
|
||||||
|
|
||||||
|
// don't draw a border unless there is enough space
|
||||||
|
let border = if width
|
||||||
|
.strip_suffix("px")
|
||||||
|
.and_then(|w| w.parse::<i32>().ok())
|
||||||
|
.filter(|w| *w < 3)
|
||||||
|
.is_some()
|
||||||
|
|| height
|
||||||
|
.strip_suffix("px")
|
||||||
|
.and_then(|h| h.parse::<i32>().ok())
|
||||||
|
.filter(|h| *h < 3)
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
"none"
|
||||||
|
} else {
|
||||||
|
"solid"
|
||||||
|
};
|
||||||
|
|
||||||
|
render! {
|
||||||
|
div{
|
||||||
|
width: "{width}",
|
||||||
|
height: "{height}",
|
||||||
|
border_style: "{border}",
|
||||||
|
|
||||||
|
onkeydown: move |k| {
|
||||||
|
if k.key()== Key::Enter {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut text = text_ref.write();
|
||||||
|
cursor.write().handle_input(&k, &mut text, max_len);
|
||||||
|
if let Some(input_handler) = &cx.props.raw_oninput{
|
||||||
|
input_handler.call(FormData{
|
||||||
|
value: text.clone(),
|
||||||
|
values: HashMap::new(),
|
||||||
|
files: None
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let node = tui_query.get(get_root_id(cx).unwrap());
|
||||||
|
let Point{ x, y } = node.pos().unwrap();
|
||||||
|
|
||||||
|
let Pos { col, row } = cursor.read().start;
|
||||||
|
let (x, y) = (col as u16 + x as u16 + if border == "none" {0} else {1}, row as u16 + y as u16 + if border == "none" {0} else {1});
|
||||||
|
if let Ok(pos) = crossterm::cursor::position() {
|
||||||
|
if pos != (x, y){
|
||||||
|
execute!(stdout(), MoveTo(x, y)).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
execute!(stdout(), MoveTo(x, y)).unwrap();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onmousemove: move |evt| {
|
||||||
|
if *dragging.get() {
|
||||||
|
let offset = evt.data.element_coordinates();
|
||||||
|
let mut new = Pos::new(offset.x as usize, offset.y as usize);
|
||||||
|
if border != "none" {
|
||||||
|
new.col = new.col.saturating_sub(1);
|
||||||
|
}
|
||||||
|
// textboxs are only one line tall
|
||||||
|
new.row = 0;
|
||||||
|
|
||||||
|
if new != cursor.read().start {
|
||||||
|
cursor.write().end = Some(new);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onmousedown: move |evt| {
|
||||||
|
let offset = evt.data.element_coordinates();
|
||||||
|
let mut new = Pos::new(offset.x as usize, offset.y as usize);
|
||||||
|
if border != "none" {
|
||||||
|
new.col = new.col.saturating_sub(1);
|
||||||
|
}
|
||||||
|
// textboxs are only one line tall
|
||||||
|
new.row = 0;
|
||||||
|
|
||||||
|
new.realize_col(&text_ref.read());
|
||||||
|
cursor.set(Cursor::from_start(new));
|
||||||
|
dragging.set(true);
|
||||||
|
let node = tui_query_clone.get(get_root_id(cx).unwrap());
|
||||||
|
let Point{ x, y } = node.pos().unwrap();
|
||||||
|
|
||||||
|
let Pos { col, row } = cursor.read().start;
|
||||||
|
let (x, y) = (col as u16 + x as u16 + if border == "none" {0} else {1}, row as u16 + y as u16 + if border == "none" {0} else {1});
|
||||||
|
if let Ok(pos) = crossterm::cursor::position() {
|
||||||
|
if pos != (x, y){
|
||||||
|
execute!(stdout(), MoveTo(x, y)).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
execute!(stdout(), MoveTo(x, y)).unwrap();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onmouseup: move |_| {
|
||||||
|
dragging.set(false);
|
||||||
|
},
|
||||||
|
onmouseleave: move |_| {
|
||||||
|
dragging.set(false);
|
||||||
|
},
|
||||||
|
onmouseenter: move |_| {
|
||||||
|
dragging.set(false);
|
||||||
|
},
|
||||||
|
onfocusout: |_| {
|
||||||
|
execute!(stdout(), MoveTo(0, 1000)).unwrap();
|
||||||
|
},
|
||||||
|
|
||||||
|
"{text_before_first_cursor}"
|
||||||
|
|
||||||
|
span{
|
||||||
|
background_color: "rgba(255, 255, 255, 50%)",
|
||||||
|
|
||||||
|
"{text_highlighted}"
|
||||||
|
}
|
||||||
|
|
||||||
|
"{text_after_second_cursor}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::widgets::get_root_id;
|
||||||
|
use crate::Query;
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_elements::input_data::keyboard_types::Key;
|
||||||
|
use dioxus_html as dioxus_elements;
|
||||||
|
use dioxus_html::FormData;
|
||||||
|
|
||||||
|
#[derive(Props)]
|
||||||
|
pub(crate) struct SliderProps<'a> {
|
||||||
|
#[props(!optional)]
|
||||||
|
raw_oninput: Option<&'a EventHandler<'a, FormData>>,
|
||||||
|
#[props(!optional)]
|
||||||
|
value: Option<&'a str>,
|
||||||
|
#[props(!optional)]
|
||||||
|
width: Option<&'a str>,
|
||||||
|
#[props(!optional)]
|
||||||
|
height: Option<&'a str>,
|
||||||
|
#[props(!optional)]
|
||||||
|
min: Option<&'a str>,
|
||||||
|
#[props(!optional)]
|
||||||
|
max: Option<&'a str>,
|
||||||
|
#[props(!optional)]
|
||||||
|
step: Option<&'a str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub(crate) fn Slider<'a>(cx: Scope<'a, SliderProps>) -> Element<'a> {
|
||||||
|
let tui_query: Query = cx.consume_context().unwrap();
|
||||||
|
|
||||||
|
let value_state = use_state(cx, || 0.0);
|
||||||
|
let value: Option<f32> = cx.props.value.and_then(|v| v.parse().ok());
|
||||||
|
let width = cx.props.width.unwrap_or("20px");
|
||||||
|
let height = cx.props.height.unwrap_or("1px");
|
||||||
|
let min = cx.props.min.and_then(|v| v.parse().ok()).unwrap_or(0.0);
|
||||||
|
let max = cx.props.max.and_then(|v| v.parse().ok()).unwrap_or(100.0);
|
||||||
|
let size = max - min;
|
||||||
|
let step = cx
|
||||||
|
.props
|
||||||
|
.step
|
||||||
|
.and_then(|v| v.parse().ok())
|
||||||
|
.unwrap_or(size / 10.0);
|
||||||
|
|
||||||
|
let current_value = if let Some(value) = value {
|
||||||
|
value
|
||||||
|
} else {
|
||||||
|
*value_state.get()
|
||||||
|
}
|
||||||
|
.max(min)
|
||||||
|
.min(max);
|
||||||
|
let fst_width = 100.0 * (current_value - min) / size;
|
||||||
|
let snd_width = 100.0 * (max - current_value) / size;
|
||||||
|
assert!(fst_width + snd_width > 99.0 && fst_width + snd_width < 101.0);
|
||||||
|
|
||||||
|
let update = |value: String| {
|
||||||
|
if let Some(oninput) = cx.props.raw_oninput {
|
||||||
|
oninput.call(FormData {
|
||||||
|
value,
|
||||||
|
values: HashMap::new(),
|
||||||
|
files: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render! {
|
||||||
|
div{
|
||||||
|
width: "{width}",
|
||||||
|
height: "{height}",
|
||||||
|
display: "flex",
|
||||||
|
flex_direction: "row",
|
||||||
|
onkeydown: move |event| {
|
||||||
|
match event.key() {
|
||||||
|
Key::ArrowLeft => {
|
||||||
|
value_state.set((current_value - step).max(min).min(max));
|
||||||
|
update(value_state.current().to_string());
|
||||||
|
}
|
||||||
|
Key::ArrowRight => {
|
||||||
|
value_state.set((current_value + step).max(min).min(max));
|
||||||
|
update(value_state.current().to_string());
|
||||||
|
}
|
||||||
|
_ => ()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onmousemove: move |evt| {
|
||||||
|
let mouse = evt.data;
|
||||||
|
if !mouse.held_buttons().is_empty(){
|
||||||
|
let node = tui_query.get(get_root_id(cx).unwrap());
|
||||||
|
let width = node.size().unwrap().width;
|
||||||
|
let offset = mouse.element_coordinates();
|
||||||
|
value_state.set(min + size*(offset.x as f32) / width as f32);
|
||||||
|
update(value_state.current().to_string());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
div{
|
||||||
|
width: "{fst_width}%",
|
||||||
|
background_color: "rgba(10,10,10,0.5)",
|
||||||
|
}
|
||||||
|
div{
|
||||||
|
"|"
|
||||||
|
}
|
||||||
|
div{
|
||||||
|
width: "{snd_width}%",
|
||||||
|
background_color: "rgba(10,10,10,0.5)",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
use crate::widgets::get_root_id;
|
||||||
|
use crate::Query;
|
||||||
|
use crossterm::{cursor::*, execute};
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_elements::input_data::keyboard_types::Key;
|
||||||
|
use dioxus_html as dioxus_elements;
|
||||||
|
use dioxus_html::FormData;
|
||||||
|
use dioxus_native_core::utils::cursor::{Cursor, Pos};
|
||||||
|
use std::{collections::HashMap, io::stdout};
|
||||||
|
use taffy::geometry::Point;
|
||||||
|
|
||||||
|
#[derive(Props)]
|
||||||
|
pub(crate) struct TextBoxProps<'a> {
|
||||||
|
#[props(!optional)]
|
||||||
|
raw_oninput: Option<&'a EventHandler<'a, FormData>>,
|
||||||
|
#[props(!optional)]
|
||||||
|
value: Option<&'a str>,
|
||||||
|
#[props(!optional)]
|
||||||
|
size: Option<&'a str>,
|
||||||
|
#[props(!optional)]
|
||||||
|
max_length: Option<&'a str>,
|
||||||
|
#[props(!optional)]
|
||||||
|
width: Option<&'a str>,
|
||||||
|
#[props(!optional)]
|
||||||
|
height: Option<&'a str>,
|
||||||
|
}
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub(crate) fn TextBox<'a>(cx: Scope<'a, TextBoxProps>) -> Element<'a> {
|
||||||
|
let tui_query: Query = cx.consume_context().unwrap();
|
||||||
|
let tui_query_clone = tui_query.clone();
|
||||||
|
|
||||||
|
let text_ref = use_ref(cx, || {
|
||||||
|
if let Some(intial_text) = cx.props.value {
|
||||||
|
intial_text.to_string()
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let cursor = use_ref(cx, Cursor::default);
|
||||||
|
let dragging = use_state(cx, || false);
|
||||||
|
|
||||||
|
let text = text_ref.read().clone();
|
||||||
|
let start_highlight = cursor.read().first().idx(&text);
|
||||||
|
let end_highlight = cursor.read().last().idx(&text);
|
||||||
|
let (text_before_first_cursor, text_after_first_cursor) = text.split_at(start_highlight);
|
||||||
|
let (text_highlighted, text_after_second_cursor) =
|
||||||
|
text_after_first_cursor.split_at(end_highlight - start_highlight);
|
||||||
|
|
||||||
|
let max_len = cx
|
||||||
|
.props
|
||||||
|
.max_length
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|s| s.parse().ok())
|
||||||
|
.unwrap_or(usize::MAX);
|
||||||
|
|
||||||
|
let width = cx
|
||||||
|
.props
|
||||||
|
.width
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
// px is the same as em in tui
|
||||||
|
.or_else(|| cx.props.size.map(|s| s.to_string() + "px"))
|
||||||
|
.unwrap_or_else(|| "10px".to_string());
|
||||||
|
let height = cx.props.height.unwrap_or("3px");
|
||||||
|
|
||||||
|
// don't draw a border unless there is enough space
|
||||||
|
let border = if width
|
||||||
|
.strip_suffix("px")
|
||||||
|
.and_then(|w| w.parse::<i32>().ok())
|
||||||
|
.filter(|w| *w < 3)
|
||||||
|
.is_some()
|
||||||
|
|| height
|
||||||
|
.strip_suffix("px")
|
||||||
|
.and_then(|h| h.parse::<i32>().ok())
|
||||||
|
.filter(|h| *h < 3)
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
"none"
|
||||||
|
} else {
|
||||||
|
"solid"
|
||||||
|
};
|
||||||
|
|
||||||
|
render! {
|
||||||
|
div{
|
||||||
|
width: "{width}",
|
||||||
|
height: "{height}",
|
||||||
|
border_style: "{border}",
|
||||||
|
|
||||||
|
onkeydown: move |k| {
|
||||||
|
if k.key() == Key::Enter {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut text = text_ref.write();
|
||||||
|
cursor.write().handle_input(&k, &mut text, max_len);
|
||||||
|
if let Some(input_handler) = &cx.props.raw_oninput{
|
||||||
|
input_handler.call(FormData{
|
||||||
|
value: text.clone(),
|
||||||
|
values: HashMap::new(),
|
||||||
|
files: None
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let node = tui_query.get(get_root_id(cx).unwrap());
|
||||||
|
let Point{ x, y } = node.pos().unwrap();
|
||||||
|
|
||||||
|
let Pos { col, row } = cursor.read().start;
|
||||||
|
let (x, y) = (col as u16 + x as u16 + if border == "none" {0} else {1}, row as u16 + y as u16 + if border == "none" {0} else {1});
|
||||||
|
if let Ok(pos) = crossterm::cursor::position() {
|
||||||
|
if pos != (x, y){
|
||||||
|
execute!(stdout(), MoveTo(x, y)).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
execute!(stdout(), MoveTo(x, y)).unwrap();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onmousemove: move |evt| {
|
||||||
|
if *dragging.get() {
|
||||||
|
let offset = evt.data.element_coordinates();
|
||||||
|
let mut new = Pos::new(offset.x as usize, offset.y as usize);
|
||||||
|
if border != "none" {
|
||||||
|
new.col = new.col.saturating_sub(1);
|
||||||
|
}
|
||||||
|
// textboxs are only one line tall
|
||||||
|
new.row = 0;
|
||||||
|
|
||||||
|
if new != cursor.read().start {
|
||||||
|
cursor.write().end = Some(new);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onmousedown: move |evt| {
|
||||||
|
let offset = evt.data.element_coordinates();
|
||||||
|
let mut new = Pos::new(offset.x as usize, offset.y as usize);
|
||||||
|
if border != "none" {
|
||||||
|
new.col = new.col.saturating_sub(1);
|
||||||
|
}
|
||||||
|
// textboxs are only one line tall
|
||||||
|
new.row = 0;
|
||||||
|
|
||||||
|
new.realize_col(&text_ref.read());
|
||||||
|
cursor.set(Cursor::from_start(new));
|
||||||
|
dragging.set(true);
|
||||||
|
let node = tui_query_clone.get(get_root_id(cx).unwrap());
|
||||||
|
let Point{ x, y } = node.pos().unwrap();
|
||||||
|
|
||||||
|
let Pos { col, row } = cursor.read().start;
|
||||||
|
let (x, y) = (col as u16 + x as u16 + if border == "none" {0} else {1}, row as u16 + y as u16 + if border == "none" {0} else {1});
|
||||||
|
if let Ok(pos) = crossterm::cursor::position() {
|
||||||
|
if pos != (x, y){
|
||||||
|
execute!(stdout(), MoveTo(x, y)).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
execute!(stdout(), MoveTo(x, y)).unwrap();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onmouseup: move |_| {
|
||||||
|
dragging.set(false);
|
||||||
|
},
|
||||||
|
onmouseleave: move |_| {
|
||||||
|
dragging.set(false);
|
||||||
|
},
|
||||||
|
onmouseenter: move |_| {
|
||||||
|
dragging.set(false);
|
||||||
|
},
|
||||||
|
onfocusout: |_| {
|
||||||
|
execute!(stdout(), MoveTo(0, 1000)).unwrap();
|
||||||
|
},
|
||||||
|
|
||||||
|
"{text_before_first_cursor}"
|
||||||
|
|
||||||
|
span{
|
||||||
|
background_color: "rgba(255, 255, 255, 50%)",
|
||||||
|
|
||||||
|
"{text_highlighted}"
|
||||||
|
}
|
||||||
|
|
||||||
|
"{text_after_second_cursor}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
|
|||||||
dioxus-core = { path = "../core", version = "^0.2.1", features = ["serialize"] }
|
dioxus-core = { path = "../core", version = "^0.2.1", features = ["serialize"] }
|
||||||
dioxus-html = { path = "../html", version = "^0.2.1", features = ["wasm-bind"] }
|
dioxus-html = { path = "../html", version = "^0.2.1", features = ["wasm-bind"] }
|
||||||
dioxus-interpreter-js = { path = "../interpreter", version = "^0.2.1", features = [
|
dioxus-interpreter-js = { path = "../interpreter", version = "^0.2.1", features = [
|
||||||
"web"
|
"sledgehammer"
|
||||||
] }
|
] }
|
||||||
|
|
||||||
js-sys = "0.3.56"
|
js-sys = "0.3.56"
|
||||||
@@ -30,6 +30,7 @@ futures-util = "0.3.19"
|
|||||||
smallstr = "0.2.0"
|
smallstr = "0.2.0"
|
||||||
futures-channel = "0.3.21"
|
futures-channel = "0.3.21"
|
||||||
serde_json = { version = "1.0" }
|
serde_json = { version = "1.0" }
|
||||||
|
serde = { version = "1.0" }
|
||||||
serde-wasm-bindgen = "0.4.5"
|
serde-wasm-bindgen = "0.4.5"
|
||||||
|
|
||||||
[dependencies.web-sys]
|
[dependencies.web-sys]
|
||||||
|
|||||||
+112
-38
@@ -7,20 +7,22 @@
|
|||||||
//! - tests to ensure dyn_into works for various event types.
|
//! - tests to ensure dyn_into works for various event types.
|
||||||
//! - Partial delegation?>
|
//! - Partial delegation?>
|
||||||
|
|
||||||
use dioxus_core::{Mutation, Template};
|
use dioxus_core::{Mutation, Template, TemplateAttribute, TemplateNode};
|
||||||
use dioxus_html::{event_bubbles, CompositionData, FormData};
|
use dioxus_html::{event_bubbles, CompositionData, FormData};
|
||||||
use dioxus_interpreter_js::Interpreter;
|
use dioxus_interpreter_js::{save_template, Channel};
|
||||||
use futures_channel::mpsc;
|
use futures_channel::mpsc;
|
||||||
|
use rustc_hash::FxHashMap;
|
||||||
use std::{any::Any, rc::Rc};
|
use std::{any::Any, rc::Rc};
|
||||||
use wasm_bindgen::{closure::Closure, JsCast};
|
use wasm_bindgen::{closure::Closure, JsCast};
|
||||||
use web_sys::{Document, Element, Event};
|
use web_sys::{Document, Element, Event, HtmlElement};
|
||||||
|
|
||||||
use crate::Config;
|
use crate::Config;
|
||||||
|
|
||||||
pub struct WebsysDom {
|
pub struct WebsysDom {
|
||||||
interpreter: Interpreter,
|
document: Document,
|
||||||
handler: Closure<dyn FnMut(&Event)>,
|
templates: FxHashMap<String, u32>,
|
||||||
_root: Element,
|
max_template_id: u32,
|
||||||
|
interpreter: Channel,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WebsysDom {
|
impl WebsysDom {
|
||||||
@@ -32,67 +34,139 @@ impl WebsysDom {
|
|||||||
Some(root) => root,
|
Some(root) => root,
|
||||||
None => document.create_element("body").ok().unwrap(),
|
None => document.create_element("body").ok().unwrap(),
|
||||||
};
|
};
|
||||||
|
let interpreter = Channel::default();
|
||||||
|
|
||||||
Self {
|
let handler: Closure<dyn FnMut(&Event)> =
|
||||||
interpreter: Interpreter::new(root.clone()),
|
Closure::wrap(Box::new(move |event: &web_sys::Event| {
|
||||||
_root: root,
|
|
||||||
handler: Closure::wrap(Box::new(move |event: &web_sys::Event| {
|
|
||||||
let _ = event_channel.unbounded_send(event.clone());
|
let _ = event_channel.unbounded_send(event.clone());
|
||||||
})),
|
}));
|
||||||
|
|
||||||
|
dioxus_interpreter_js::initilize(root.unchecked_into(), handler.as_ref().unchecked_ref());
|
||||||
|
handler.forget();
|
||||||
|
Self {
|
||||||
|
document,
|
||||||
|
interpreter,
|
||||||
|
templates: FxHashMap::default(),
|
||||||
|
max_template_id: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mount(&mut self) {
|
pub fn mount(&mut self) {
|
||||||
self.interpreter.MountToRoot();
|
self.interpreter.mount_to_root();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_templates(&mut self, templates: &[Template]) {
|
pub fn load_templates(&mut self, templates: &[Template]) {
|
||||||
log::debug!("Loading templates {:?}", templates);
|
log::debug!("Loading templates {:?}", templates);
|
||||||
|
|
||||||
for template in templates {
|
for template in templates {
|
||||||
self.interpreter
|
let mut roots = vec![];
|
||||||
.SaveTemplate(serde_wasm_bindgen::to_value(&template).unwrap());
|
|
||||||
|
for root in template.roots {
|
||||||
|
roots.push(self.create_template_node(root))
|
||||||
|
}
|
||||||
|
|
||||||
|
self.templates
|
||||||
|
.insert(template.name.to_owned(), self.max_template_id);
|
||||||
|
save_template(roots, self.max_template_id);
|
||||||
|
self.max_template_id += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_template_node(&self, v: &TemplateNode) -> web_sys::Node {
|
||||||
|
use TemplateNode::*;
|
||||||
|
match v {
|
||||||
|
Element {
|
||||||
|
tag,
|
||||||
|
namespace,
|
||||||
|
attrs,
|
||||||
|
children,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
let el = match namespace {
|
||||||
|
Some(ns) => self.document.create_element_ns(Some(ns), tag).unwrap(),
|
||||||
|
None => self.document.create_element(tag).unwrap(),
|
||||||
|
};
|
||||||
|
for attr in *attrs {
|
||||||
|
if let TemplateAttribute::Static {
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
namespace,
|
||||||
|
} = attr
|
||||||
|
{
|
||||||
|
match namespace {
|
||||||
|
Some(ns) if *ns == "style" => el
|
||||||
|
.dyn_ref::<HtmlElement>()
|
||||||
|
.unwrap()
|
||||||
|
.style()
|
||||||
|
.set_property(name, value)
|
||||||
|
.unwrap(),
|
||||||
|
Some(ns) => el.set_attribute_ns(Some(ns), name, value).unwrap(),
|
||||||
|
None => el.set_attribute(name, value).unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for child in *children {
|
||||||
|
let _ = el.append_child(&self.create_template_node(child));
|
||||||
|
}
|
||||||
|
el.dyn_into().unwrap()
|
||||||
|
}
|
||||||
|
Text { text } => self.document.create_text_node(text).dyn_into().unwrap(),
|
||||||
|
DynamicText { .. } => self.document.create_text_node("p").dyn_into().unwrap(),
|
||||||
|
Dynamic { .. } => {
|
||||||
|
let el = self.document.create_element("pre").unwrap();
|
||||||
|
let _ = el.toggle_attribute("hidden");
|
||||||
|
el.dyn_into().unwrap()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn apply_edits(&mut self, mut edits: Vec<Mutation>) {
|
pub fn apply_edits(&mut self, mut edits: Vec<Mutation>) {
|
||||||
use Mutation::*;
|
use Mutation::*;
|
||||||
let i = &self.interpreter;
|
let i = &mut self.interpreter;
|
||||||
for edit in edits.drain(..) {
|
for edit in &edits {
|
||||||
match edit {
|
match edit {
|
||||||
AppendChildren { id, m } => i.AppendChildren(m as u32, id.0 as u32),
|
AppendChildren { id, m } => i.append_children(id.0 as u32, *m as u32),
|
||||||
AssignId { path, id } => i.AssignId(path, id.0 as u32),
|
AssignId { path, id } => {
|
||||||
CreatePlaceholder { id } => i.CreatePlaceholder(id.0 as u32),
|
i.assign_id(path.as_ptr() as u32, path.len() as u8, id.0 as u32)
|
||||||
CreateTextNode { value, id } => i.CreateTextNode(value.into(), id.0 as u32),
|
}
|
||||||
HydrateText { path, value, id } => i.HydrateText(path, value, id.0 as u32),
|
CreatePlaceholder { id } => i.create_placeholder(id.0 as u32),
|
||||||
LoadTemplate { name, index, id } => i.LoadTemplate(name, index as u32, id.0 as u32),
|
CreateTextNode { value, id } => i.create_text_node(value, id.0 as u32),
|
||||||
ReplaceWith { id, m } => i.ReplaceWith(id.0 as u32, m as u32),
|
HydrateText { path, value, id } => {
|
||||||
ReplacePlaceholder { path, m } => i.ReplacePlaceholder(path, m as u32),
|
i.hydrate_text(path.as_ptr() as u32, path.len() as u8, value, id.0 as u32)
|
||||||
InsertAfter { id, m } => i.InsertAfter(id.0 as u32, m as u32),
|
}
|
||||||
InsertBefore { id, m } => i.InsertBefore(id.0 as u32, m as u32),
|
LoadTemplate { name, index, id } => {
|
||||||
|
if let Some(tmpl_id) = self.templates.get(*name) {
|
||||||
|
i.load_template(*tmpl_id, *index as u32, id.0 as u32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ReplaceWith { id, m } => i.replace_with(id.0 as u32, *m as u32),
|
||||||
|
ReplacePlaceholder { path, m } => {
|
||||||
|
i.replace_placeholder(path.as_ptr() as u32, path.len() as u8, *m as u32)
|
||||||
|
}
|
||||||
|
InsertAfter { id, m } => i.insert_after(id.0 as u32, *m as u32),
|
||||||
|
InsertBefore { id, m } => i.insert_before(id.0 as u32, *m as u32),
|
||||||
SetAttribute {
|
SetAttribute {
|
||||||
name,
|
name,
|
||||||
value,
|
value,
|
||||||
id,
|
id,
|
||||||
ns,
|
ns,
|
||||||
} => i.SetAttribute(id.0 as u32, name, value.into(), ns),
|
} => i.set_attribute(id.0 as u32, name, value, ns.unwrap_or_default()),
|
||||||
SetBoolAttribute { name, value, id } => {
|
SetBoolAttribute { name, value, id } => {
|
||||||
i.SetBoolAttribute(id.0 as u32, name, value)
|
i.set_attribute(id.0 as u32, name, if *value { "true" } else { "false" }, "")
|
||||||
}
|
}
|
||||||
SetText { value, id } => i.SetText(id.0 as u32, value.into()),
|
SetText { value, id } => i.set_text(id.0 as u32, value),
|
||||||
NewEventListener { name, id, .. } => {
|
NewEventListener { name, id, .. } => {
|
||||||
self.interpreter.NewEventListener(
|
i.new_event_listener(name, id.0 as u32, event_bubbles(&name[2..]) as u8);
|
||||||
name,
|
|
||||||
id.0 as u32,
|
|
||||||
event_bubbles(&name[2..]),
|
|
||||||
self.handler.as_ref().unchecked_ref(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
RemoveEventListener { name, id } => i.RemoveEventListener(name, id.0 as u32),
|
RemoveEventListener { name, id } => {
|
||||||
Remove { id } => i.Remove(id.0 as u32),
|
i.remove_event_listener(name, id.0 as u32, event_bubbles(&name[2..]) as u8)
|
||||||
PushRoot { id } => i.PushRoot(id.0 as u32),
|
}
|
||||||
|
Remove { id } => i.remove(id.0 as u32),
|
||||||
|
PushRoot { id } => i.push_root(id.0 as u32),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
edits.clear();
|
||||||
|
i.flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
|
|
||||||
pub use crate::cfg::Config;
|
pub use crate::cfg::Config;
|
||||||
use crate::dom::virtual_event_from_websys_event;
|
use crate::dom::virtual_event_from_websys_event;
|
||||||
pub use crate::util::use_eval;
|
pub use crate::util::{use_eval, EvalResult};
|
||||||
use dioxus_core::{Element, ElementId, Scope, VirtualDom};
|
use dioxus_core::{Element, ElementId, Scope, VirtualDom};
|
||||||
use futures_util::{pin_mut, FutureExt, StreamExt};
|
use futures_util::{pin_mut, FutureExt, StreamExt};
|
||||||
|
|
||||||
@@ -195,7 +195,7 @@ pub async fn run_with_props<T: 'static>(root: fn(Scope<T>) -> Element, root_prop
|
|||||||
// the mutations come back with nothing - we need to actually mount them
|
// the mutations come back with nothing - we need to actually mount them
|
||||||
websys_dom.mount();
|
websys_dom.mount();
|
||||||
|
|
||||||
let mut work_loop = ric_raf::RafLoop::new();
|
let _work_loop = ric_raf::RafLoop::new();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
log::debug!("waiting for work");
|
log::debug!("waiting for work");
|
||||||
@@ -228,6 +228,7 @@ pub async fn run_with_props<T: 'static>(root: fn(Scope<T>) -> Element, root_prop
|
|||||||
res = rx.try_next().transpose().unwrap().ok();
|
res = rx.try_next().transpose().unwrap().ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Todo: This is currently disabled because it has a negative impact on responce times for events but it could be re-enabled for tasks
|
||||||
// Jank free rendering
|
// Jank free rendering
|
||||||
//
|
//
|
||||||
// 1. wait for the browser to give us "idle" time
|
// 1. wait for the browser to give us "idle" time
|
||||||
@@ -236,13 +237,13 @@ pub async fn run_with_props<T: 'static>(root: fn(Scope<T>) -> Element, root_prop
|
|||||||
// 4. Wait for the animation frame to patch the dom
|
// 4. Wait for the animation frame to patch the dom
|
||||||
|
|
||||||
// wait for the mainthread to schedule us in
|
// wait for the mainthread to schedule us in
|
||||||
let deadline = work_loop.wait_for_idle_time().await;
|
// let deadline = work_loop.wait_for_idle_time().await;
|
||||||
|
|
||||||
// run the virtualdom work phase until the frame deadline is reached
|
// run the virtualdom work phase until the frame deadline is reached
|
||||||
let edits = dom.render_with_deadline(deadline).await;
|
let edits = dom.render_immediate();
|
||||||
|
|
||||||
// wait for the animation frame to fire so we can apply our changes
|
// wait for the animation frame to fire so we can apply our changes
|
||||||
work_loop.wait_for_raf().await;
|
// work_loop.wait_for_raf().await;
|
||||||
|
|
||||||
websys_dom.load_templates(&edits.templates);
|
websys_dom.load_templates(&edits.templates);
|
||||||
websys_dom.apply_edits(edits.edits);
|
websys_dom.apply_edits(edits.edits);
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
//! Utilities specific to websys
|
//! Utilities specific to websys
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
future::{IntoFuture, Ready},
|
||||||
|
str::FromStr,
|
||||||
|
};
|
||||||
|
|
||||||
use dioxus_core::*;
|
use dioxus_core::*;
|
||||||
|
use serde::de::Error;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
/// Get a closure that executes any JavaScript in the webpage.
|
/// Get a closure that executes any JavaScript in the webpage.
|
||||||
///
|
///
|
||||||
@@ -15,12 +22,51 @@ use dioxus_core::*;
|
|||||||
///
|
///
|
||||||
/// The closure will panic if the provided script is not valid JavaScript code
|
/// The closure will panic if the provided script is not valid JavaScript code
|
||||||
/// or if it returns an uncaught error.
|
/// or if it returns an uncaught error.
|
||||||
pub fn use_eval<S: std::string::ToString>(cx: &ScopeState) -> &dyn Fn(S) {
|
pub fn use_eval<S: std::string::ToString>(cx: &ScopeState) -> &dyn Fn(S) -> EvalResult {
|
||||||
cx.use_hook(|| {
|
cx.use_hook(|| {
|
||||||
|script: S| {
|
|script: S| {
|
||||||
js_sys::Function::new_no_args(&script.to_string())
|
let body = script.to_string();
|
||||||
.call0(&wasm_bindgen::JsValue::NULL)
|
EvalResult {
|
||||||
.expect("failed to eval script");
|
value: if let Ok(value) =
|
||||||
|
js_sys::Function::new_no_args(&body).call0(&wasm_bindgen::JsValue::NULL)
|
||||||
|
{
|
||||||
|
if let Ok(stringified) = js_sys::JSON::stringify(&value) {
|
||||||
|
if !stringified.is_undefined() && stringified.is_valid_utf16() {
|
||||||
|
let string: String = stringified.into();
|
||||||
|
Value::from_str(&string)
|
||||||
|
} else {
|
||||||
|
Err(serde_json::Error::custom("Failed to stringify result"))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(serde_json::Error::custom("Failed to stringify result"))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(serde_json::Error::custom("Failed to execute script"))
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A wrapper around the result of a JavaScript evaluation.
|
||||||
|
/// This implements IntoFuture to be compatible with the desktop renderer's EvalResult.
|
||||||
|
pub struct EvalResult {
|
||||||
|
value: Result<Value, serde_json::Error>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EvalResult {
|
||||||
|
/// Get the result of the Javascript execution.
|
||||||
|
pub fn get(self) -> Result<Value, serde_json::Error> {
|
||||||
|
self.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoFuture for EvalResult {
|
||||||
|
type Output = Result<Value, serde_json::Error>;
|
||||||
|
|
||||||
|
type IntoFuture = Ready<Result<Value, serde_json::Error>>;
|
||||||
|
|
||||||
|
fn into_future(self) -> Self::IntoFuture {
|
||||||
|
std::future::ready(self.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user