Merge branch 'master' into server-fn

This commit is contained in:
Jon Kelley authored and GitHub committed 2023-05-18 13:18:41 +02:00
commit 6bd3437e3a
78 files changed
+3946 -445

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "dioxus-core"
version = "0.3.2"
version = "0.3.3"
authors = ["Jonathan Kelley"]
edition = "2018"
description = "Core functionality for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences"
+14 -1
View File
@@ -57,4 +57,17 @@ hot-reload = ["dioxus-hot-reload"]
[dev-dependencies]
dioxus-core-macro = { path = "../core-macro" }
dioxus-hooks = { path = "../hooks" }
# image = "0.24.0" # enable this when generating a new desktop image
dioxus = { path = "../dioxus" }
exitcode = "1.1.2"
scraper = "0.16.0"
# These tests need to be run on the main thread, so they cannot use rust's test harness.
[[test]]
name = "check_events"
path = "headless_tests/events.rs"
harness = false
[[test]]
name = "check_rendering"
path = "headless_tests/rendering.rs"
harness = false
+351
View File
@@ -0,0 +1,351 @@
use dioxus::html::geometry::euclid::Vector3D;
use dioxus::prelude::*;
use dioxus_desktop::DesktopContext;
pub(crate) fn check_app_exits(app: Component) {
use dioxus_desktop::tao::window::WindowBuilder;
use dioxus_desktop::Config;
// This is a deadman's switch to ensure that the app exits
let should_panic = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true));
let should_panic_clone = should_panic.clone();
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_secs(100));
if should_panic_clone.load(std::sync::atomic::Ordering::SeqCst) {
std::process::exit(exitcode::SOFTWARE);
}
});
dioxus_desktop::launch_cfg(
app,
Config::new().with_window(WindowBuilder::new().with_visible(false)),
);
should_panic.store(false, std::sync::atomic::Ordering::SeqCst);
}
pub fn main() {
check_app_exits(app);
}
fn mock_event(cx: &ScopeState, id: &'static str, value: &'static str) {
use_effect(cx, (), |_| {
let desktop_context: DesktopContext = cx.consume_context().unwrap();
async move {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
desktop_context.eval(&format!(
r#"let element = document.getElementById('{}');
// Dispatch a synthetic event
const event = {};
console.log(element, event);
element.dispatchEvent(event);
"#,
id, value
));
}
});
}
#[allow(deprecated)]
fn app(cx: Scope) -> Element {
let desktop_context: DesktopContext = cx.consume_context().unwrap();
let recieved_events = use_state(cx, || 0);
// button
mock_event(
cx,
"button",
r#"new MouseEvent("click", {
view: window,
bubbles: true,
cancelable: true,
button: 0,
})"#,
);
// mouse_move_div
mock_event(
cx,
"mouse_move_div",
r#"new MouseEvent("mousemove", {
view: window,
bubbles: true,
cancelable: true,
buttons: 2,
})"#,
);
// mouse_click_div
mock_event(
cx,
"mouse_click_div",
r#"new MouseEvent("click", {
view: window,
bubbles: true,
cancelable: true,
buttons: 2,
button: 2,
})"#,
);
// mouse_dblclick_div
mock_event(
cx,
"mouse_dblclick_div",
r#"new MouseEvent("dblclick", {
view: window,
bubbles: true,
cancelable: true,
buttons: 1|2,
button: 2,
})"#,
);
// mouse_down_div
mock_event(
cx,
"mouse_down_div",
r#"new MouseEvent("mousedown", {
view: window,
bubbles: true,
cancelable: true,
buttons: 2,
button: 2,
})"#,
);
// mouse_up_div
mock_event(
cx,
"mouse_up_div",
r#"new MouseEvent("mouseup", {
view: window,
bubbles: true,
cancelable: true,
buttons: 0,
button: 0,
})"#,
);
// wheel_div
mock_event(
cx,
"wheel_div",
r#"new WheelEvent("wheel", {
view: window,
deltaX: 1.0,
deltaY: 2.0,
deltaZ: 3.0,
deltaMode: 0x00,
bubbles: true,
})"#,
);
// key_down_div
mock_event(
cx,
"key_down_div",
r#"new KeyboardEvent("keydown", {
key: "a",
code: "KeyA",
location: 0,
repeat: true,
keyCode: 65,
charCode: 97,
char: "a",
charCode: 0,
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: false,
isComposing: false,
which: 65,
bubbles: true,
})"#,
);
// key_up_div
mock_event(
cx,
"key_up_div",
r#"new KeyboardEvent("keyup", {
key: "a",
code: "KeyA",
location: 0,
repeat: false,
keyCode: 65,
charCode: 97,
char: "a",
charCode: 0,
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: false,
isComposing: false,
which: 65,
bubbles: true,
})"#,
);
// key_press_div
mock_event(
cx,
"key_press_div",
r#"new KeyboardEvent("keypress", {
key: "a",
code: "KeyA",
location: 0,
repeat: false,
keyCode: 65,
charCode: 97,
char: "a",
charCode: 0,
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: false,
isComposing: false,
which: 65,
bubbles: true,
})"#,
);
// focus_in_div
mock_event(
cx,
"focus_in_div",
r#"new FocusEvent("focusin", {bubbles: true})"#,
);
// focus_out_div
mock_event(
cx,
"focus_out_div",
r#"new FocusEvent("focusout",{bubbles: true})"#,
);
if **recieved_events == 12 {
println!("all events recieved");
desktop_context.close();
}
cx.render(rsx! {
div {
button {
id: "button",
onclick: move |event| {
println!("{:?}", event.data);
assert!(event.data.modifiers().is_empty());
assert!(event.data.held_buttons().is_empty());
assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Primary));
recieved_events.modify(|x| *x + 1)
},
}
div {
id: "mouse_move_div",
onmousemove: move |event| {
println!("{:?}", event.data);
assert!(event.data.modifiers().is_empty());
assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary));
recieved_events.modify(|x| *x + 1)
},
}
div {
id: "mouse_click_div",
onclick: move |event| {
println!("{:?}", event.data);
assert!(event.data.modifiers().is_empty());
assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary));
assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Secondary));
recieved_events.modify(|x| *x + 1)
},
}
div{
id: "mouse_dblclick_div",
ondblclick: move |event| {
println!("{:?}", event.data);
assert!(event.data.modifiers().is_empty());
assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Primary));
assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary));
assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Secondary));
recieved_events.modify(|x| *x + 1)
}
}
div{
id: "mouse_down_div",
onmousedown: move |event| {
println!("{:?}", event.data);
assert!(event.data.modifiers().is_empty());
assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary));
assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Secondary));
recieved_events.modify(|x| *x + 1)
}
}
div{
id: "mouse_up_div",
onmouseup: move |event| {
println!("{:?}", event.data);
assert!(event.data.modifiers().is_empty());
assert!(event.data.held_buttons().is_empty());
assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Primary));
recieved_events.modify(|x| *x + 1)
}
}
div{
id: "wheel_div",
width: "100px",
height: "100px",
background_color: "red",
onwheel: move |event| {
println!("{:?}", event.data);
let dioxus_html::geometry::WheelDelta::Pixels(delta)= event.data.delta()else{
panic!("Expected delta to be in pixels")
};
assert_eq!(delta, Vector3D::new(1.0, 2.0, 3.0));
recieved_events.modify(|x| *x + 1)
}
}
input{
id: "key_down_div",
onkeydown: move |event| {
println!("{:?}", event.data);
assert!(event.data.modifiers().is_empty());
assert_eq!(event.data.key().to_string(), "a");
assert_eq!(event.data.code().to_string(), "KeyA");
assert_eq!(event.data.location, 0);
assert!(event.data.is_auto_repeating());
recieved_events.modify(|x| *x + 1)
}
}
input{
id: "key_up_div",
onkeyup: move |event| {
println!("{:?}", event.data);
assert!(event.data.modifiers().is_empty());
assert_eq!(event.data.key().to_string(), "a");
assert_eq!(event.data.code().to_string(), "KeyA");
assert_eq!(event.data.location, 0);
assert!(!event.data.is_auto_repeating());
recieved_events.modify(|x| *x + 1)
}
}
input{
id: "key_press_div",
onkeypress: move |event| {
println!("{:?}", event.data);
assert!(event.data.modifiers().is_empty());
assert_eq!(event.data.key().to_string(), "a");
assert_eq!(event.data.code().to_string(), "KeyA");
assert_eq!(event.data.location, 0);
assert!(!event.data.is_auto_repeating());
recieved_events.modify(|x| *x + 1)
}
}
input{
id: "focus_in_div",
onfocusin: move |event| {
println!("{:?}", event.data);
recieved_events.modify(|x| *x + 1)
}
}
input{
id: "focus_out_div",
onfocusout: move |event| {
println!("{:?}", event.data);
recieved_events.modify(|x| *x + 1)
}
}
}
})
}
@@ -0,0 +1,94 @@
use dioxus::prelude::*;
use dioxus_desktop::DesktopContext;
pub(crate) fn check_app_exits(app: Component) {
use dioxus_desktop::tao::window::WindowBuilder;
use dioxus_desktop::Config;
// This is a deadman's switch to ensure that the app exits
let should_panic = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true));
let should_panic_clone = should_panic.clone();
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_secs(100));
if should_panic_clone.load(std::sync::atomic::Ordering::SeqCst) {
std::process::exit(exitcode::SOFTWARE);
}
});
dioxus_desktop::launch_cfg(
app,
Config::new().with_window(WindowBuilder::new().with_visible(false)),
);
should_panic.store(false, std::sync::atomic::Ordering::SeqCst);
}
fn main() {
check_app_exits(check_html_renders);
}
fn use_inner_html(cx: &ScopeState, id: &'static str) -> Option<String> {
let value: &UseRef<Option<String>> = use_ref(cx, || None);
use_effect(cx, (), |_| {
to_owned![value];
let desktop_context: DesktopContext = cx.consume_context().unwrap();
async move {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
let html = desktop_context
.eval(&format!(
r#"let element = document.getElementById('{}');
return element.innerHTML;"#,
id
))
.await;
if let Ok(serde_json::Value::String(html)) = html {
println!("html: {}", html);
value.set(Some(html));
}
}
});
value.read().clone()
}
const EXPECTED_HTML: &str = r#"<div id="5" style="width: 100px; height: 100px; color: rgb(0, 0, 0);"><input type="checkbox"><h1>text</h1><div><p>hello world</p></div></div>"#;
fn check_html_renders(cx: Scope) -> Element {
let inner_html = use_inner_html(cx, "main_div");
let desktop_context: DesktopContext = cx.consume_context().unwrap();
if let Some(raw_html) = inner_html.as_deref() {
let fragment = scraper::Html::parse_fragment(raw_html);
println!("fragment: {:?}", fragment.html());
let expected = scraper::Html::parse_fragment(EXPECTED_HTML);
println!("fragment: {:?}", expected.html());
if fragment == expected {
println!("html matches");
desktop_context.close();
}
}
let dyn_value = 0;
let dyn_element = rsx! {
div {
dangerous_inner_html: "<p>hello world</p>",
}
};
render! {
div {
id: "main_div",
div {
width: "100px",
height: "100px",
color: "rgb({dyn_value}, {dyn_value}, {dyn_value})",
id: 5,
input {
"type": "checkbox",
},
h1 {
"text"
}
dyn_element
}
}
}
}
+7 -25
View File
@@ -5,6 +5,7 @@ use std::rc::Weak;
use crate::create_new_window;
use crate::eval::EvalResult;
use crate::events::IpcMessage;
use crate::query::QueryEngine;
use crate::shortcut::IntoKeyCode;
use crate::shortcut::IntoModifersState;
use crate::shortcut::ShortcutId;
@@ -16,7 +17,6 @@ use dioxus_core::ScopeState;
use dioxus_core::VirtualDom;
#[cfg(all(feature = "hot-reload", debug_assertions))]
use dioxus_hot_reload::HotReloadMsg;
use serde_json::Value;
use slab::Slab;
use wry::application::event::Event;
use wry::application::event_loop::EventLoopProxy;
@@ -59,8 +59,8 @@ pub struct DesktopContext {
/// The proxy to the event loop
pub proxy: ProxyType,
/// The receiver for eval results since eval is async
pub(super) eval: tokio::sync::broadcast::Sender<Value>,
/// The receiver for queries about the current window
pub(super) query: QueryEngine,
pub(super) pending_windows: WebviewQueue,
@@ -96,7 +96,7 @@ impl DesktopContext {
webview,
proxy,
event_loop,
eval: tokio::sync::broadcast::channel(8).0,
query: Default::default(),
pending_windows: webviews,
event_handlers,
shortcut_manager,
@@ -210,28 +210,10 @@ impl DesktopContext {
/// Evaluate a javascript expression
pub fn eval(&self, code: &str) -> EvalResult {
// Embed the return of the eval in a function so we can send it back to the main thread
let script = format!(
r#"
window.ipc.postMessage(
JSON.stringify({{
"method":"eval_result",
"params": (
function(){{
{code}
}}
)()
}})
);
"#
);
// the query id lets us keep track of the eval result and send it back to the main thread
let query = self.query.new_query(code, &self.webview);
if let Err(e) = self.webview.evaluate_script(&script) {
// send an error to the eval receiver
log::warn!("Eval script error: {e}");
}
EvalResult::new(self.eval.clone())
EvalResult::new(query)
}
/// Create a wry event handler that listens for wry events.
+123
View File
@@ -0,0 +1,123 @@
use std::rc::Rc;
use dioxus_core::ElementId;
use dioxus_html::{geometry::euclid::Rect, MountedResult, RenderedElementBacking};
use wry::webview::WebView;
use crate::query::QueryEngine;
/// A mounted element passed to onmounted events
pub struct DesktopElement {
id: ElementId,
webview: Rc<WebView>,
query: QueryEngine,
}
impl DesktopElement {
pub(crate) fn new(id: ElementId, webview: Rc<WebView>, query: QueryEngine) -> Self {
Self { id, webview, query }
}
}
impl RenderedElementBacking for DesktopElement {
fn get_raw_element(&self) -> dioxus_html::MountedResult<&dyn std::any::Any> {
Ok(self)
}
fn get_client_rect(
&self,
) -> std::pin::Pin<
Box<
dyn futures_util::Future<
Output = dioxus_html::MountedResult<dioxus_html::geometry::euclid::Rect<f64, f64>>,
>,
>,
> {
let script = format!("return window.interpreter.GetClientRect({});", self.id.0);
let fut = self
.query
.new_query::<Option<Rect<f64, f64>>>(&script, &self.webview)
.resolve();
Box::pin(async move {
match fut.await {
Ok(Some(rect)) => Ok(rect),
Ok(None) => MountedResult::Err(dioxus_html::MountedError::OperationFailed(
Box::new(DesktopQueryError::FailedToQuery),
)),
Err(err) => {
MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err)))
}
}
})
}
fn scroll_to(
&self,
behavior: dioxus_html::ScrollBehavior,
) -> std::pin::Pin<Box<dyn futures_util::Future<Output = dioxus_html::MountedResult<()>>>> {
let script = format!(
"return window.interpreter.ScrollTo({}, {});",
self.id.0,
serde_json::to_string(&behavior).expect("Failed to serialize ScrollBehavior")
);
let fut = self
.query
.new_query::<bool>(&script, &self.webview)
.resolve();
Box::pin(async move {
match fut.await {
Ok(true) => Ok(()),
Ok(false) => MountedResult::Err(dioxus_html::MountedError::OperationFailed(
Box::new(DesktopQueryError::FailedToQuery),
)),
Err(err) => {
MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err)))
}
}
})
}
fn set_focus(
&self,
focus: bool,
) -> std::pin::Pin<Box<dyn futures_util::Future<Output = dioxus_html::MountedResult<()>>>> {
let script = format!(
"return window.interpreter.SetFocus({}, {});",
self.id.0, focus
);
let fut = self
.query
.new_query::<bool>(&script, &self.webview)
.resolve();
Box::pin(async move {
match fut.await {
Ok(true) => Ok(()),
Ok(false) => MountedResult::Err(dioxus_html::MountedError::OperationFailed(
Box::new(DesktopQueryError::FailedToQuery),
)),
Err(err) => {
MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err)))
}
}
})
}
}
#[derive(Debug)]
enum DesktopQueryError {
FailedToQuery,
}
impl std::fmt::Display for DesktopQueryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DesktopQueryError::FailedToQuery => write!(f, "Failed to query the element"),
}
}
}
impl std::error::Error for DesktopQueryError {}
+9 -13
View File
@@ -1,36 +1,32 @@
use std::rc::Rc;
use crate::query::Query;
use crate::query::QueryError;
use crate::use_window;
use dioxus_core::ScopeState;
use serde::de::Error;
use std::future::Future;
use std::future::IntoFuture;
use std::pin::Pin;
/// A future that resolves to the result of a JavaScript evaluation.
pub struct EvalResult {
pub(crate) broadcast: tokio::sync::broadcast::Sender<serde_json::Value>,
pub(crate) query: Query<serde_json::Value>,
}
impl EvalResult {
pub(crate) fn new(sender: tokio::sync::broadcast::Sender<serde_json::Value>) -> Self {
Self { broadcast: sender }
pub(crate) fn new(query: Query<serde_json::Value>) -> Self {
Self { query }
}
}
impl IntoFuture for EvalResult {
type Output = Result<serde_json::Value, serde_json::Error>;
type Output = Result<serde_json::Value, QueryError>;
type IntoFuture = Pin<Box<dyn Future<Output = Result<serde_json::Value, serde_json::Error>>>>;
type IntoFuture = Pin<Box<dyn Future<Output = Result<serde_json::Value, QueryError>>>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(async move {
let mut reciever = self.broadcast.subscribe();
match reciever.recv().await {
Ok(result) => Ok(result),
Err(_) => Err(serde_json::Error::custom("No result returned")),
}
}) as Pin<Box<dyn Future<Output = Result<serde_json::Value, serde_json::Error>>>>
Box::pin(self.query.resolve())
as Pin<Box<dyn Future<Output = Result<serde_json::Value, QueryError>>>>
}
}
+49 -18
View File
@@ -5,22 +5,27 @@
mod cfg;
mod desktop_context;
mod element;
mod escape;
mod eval;
mod events;
mod file_upload;
mod protocol;
mod query;
mod shortcut;
mod waker;
mod webview;
use crate::query::QueryResult;
pub use cfg::Config;
pub use desktop_context::{
use_window, use_wry_event_handler, DesktopContext, WryEventHandler, WryEventHandlerId,
};
use desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers};
use dioxus_core::*;
use dioxus_html::MountedData;
use dioxus_html::{native_bind::NativeFileEngine, FormData, HtmlEvent};
use element::DesktopElement;
pub use eval::{use_eval, EvalResult};
use futures_util::{pin_mut, FutureExt};
use shortcut::ShortcutRegistry;
@@ -221,39 +226,65 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
}
EventData::Ipc(msg) if msg.method() == "user_event" => {
let evt = match serde_json::from_value::<HtmlEvent>(msg.params()) {
let params = msg.params();
let evt = match serde_json::from_value::<HtmlEvent>(params) {
Ok(value) => value,
Err(_) => return,
};
let HtmlEvent {
element,
name,
bubbles,
data,
} = evt;
let view = webviews.get_mut(&event.1).unwrap();
view.dom
.handle_event(&evt.name, evt.data.into_any(), evt.element, evt.bubbles);
// check for a mounted event placeholder and replace it with a desktop specific element
let as_any = if let dioxus_html::EventData::Mounted = &data {
let query = view
.dom
.base_scope()
.consume_context::<DesktopContext>()
.unwrap()
.query;
let element = DesktopElement::new(element, view.webview.clone(), query);
Rc::new(MountedData::new(element))
} else {
data.into_any()
};
view.dom.handle_event(&name, as_any, element, bubbles);
send_edits(view.dom.render_immediate(), &view.webview);
}
// When the webview sends a query, we need to send it to the query manager which handles dispatching the data to the correct pending query
EventData::Ipc(msg) if msg.method() == "query" => {
let params = msg.params();
if let Ok(result) = serde_json::from_value::<QueryResult>(params) {
let view = webviews.get(&event.1).unwrap();
let query = view
.dom
.base_scope()
.consume_context::<DesktopContext>()
.unwrap()
.query;
query.send(result);
}
}
EventData::Ipc(msg) if msg.method() == "initialize" => {
let view = webviews.get_mut(&event.1).unwrap();
send_edits(view.dom.rebuild(), &view.webview);
}
// When the webview chirps back with the result of the eval, we send it to the active receiver
//
// This currently doesn't perform any targeting to the callsite, so if you eval multiple times at once,
// you might the wrong result. This should be fixed
EventData::Ipc(msg) if msg.method() == "eval_result" => {
webviews[&event.1]
.dom
.base_scope()
.consume_context::<DesktopContext>()
.unwrap()
.eval
.send(msg.params())
.unwrap();
}
EventData::Ipc(msg) if msg.method() == "browser_open" => {
if let Some(temp) = msg.params().as_object() {
if temp.contains_key("href") {
+110
View File
@@ -0,0 +1,110 @@
use std::{cell::RefCell, rc::Rc};
use serde::{de::DeserializeOwned, Deserialize};
use serde_json::Value;
use slab::Slab;
use thiserror::Error;
use tokio::sync::broadcast::error::RecvError;
use wry::webview::WebView;
/// Tracks what query ids are currently active
#[derive(Default, Clone)]
struct SharedSlab {
slab: Rc<RefCell<Slab<()>>>,
}
/// Handles sending and receiving arbitrary queries from the webview. Queries can be resolved non-sequentially, so we use ids to track them.
#[derive(Clone)]
pub(crate) struct QueryEngine {
sender: Rc<tokio::sync::broadcast::Sender<QueryResult>>,
active_requests: SharedSlab,
}
impl Default for QueryEngine {
fn default() -> Self {
let (sender, _) = tokio::sync::broadcast::channel(8);
Self {
sender: Rc::new(sender),
active_requests: SharedSlab::default(),
}
}
}
impl QueryEngine {
/// Creates a new query and returns a handle to it. The query will be resolved when the webview returns a result with the same id.
pub fn new_query<V: DeserializeOwned>(&self, script: &str, webview: &WebView) -> Query<V> {
let request_id = self.active_requests.slab.borrow_mut().insert(());
// start the query
// We embed the return of the eval in a function so we can send it back to the main thread
if let Err(err) = webview.evaluate_script(&format!(
r#"window.ipc.postMessage(
JSON.stringify({{
"method":"query",
"params": {{
"id": {request_id},
"data": (function(){{{script}}})()
}}
}})
);"#
)) {
log::warn!("Query error: {err}");
}
Query {
slab: self.active_requests.clone(),
id: request_id,
reciever: self.sender.subscribe(),
phantom: std::marker::PhantomData,
}
}
/// Send a query result
pub fn send(&self, data: QueryResult) {
let _ = self.sender.send(data);
}
}
pub(crate) struct Query<V: DeserializeOwned> {
slab: SharedSlab,
id: usize,
reciever: tokio::sync::broadcast::Receiver<QueryResult>,
phantom: std::marker::PhantomData<V>,
}
impl<V: DeserializeOwned> Query<V> {
/// Resolve the query
pub async fn resolve(mut self) -> Result<V, QueryError> {
let result = loop {
match self.reciever.recv().await {
Ok(result) => {
if result.id == self.id {
break V::deserialize(result.data).map_err(QueryError::DeserializeError);
}
}
Err(err) => {
break Err(QueryError::RecvError(err));
}
}
};
// Remove the query from the slab
self.slab.slab.borrow_mut().remove(self.id);
result
}
}
#[derive(Error, Debug)]
pub enum QueryError {
#[error("Error receiving query result: {0}")]
RecvError(RecvError),
#[error("Error deserializing query result: {0}")]
DeserializeError(serde_json::Error),
}
#[derive(Clone, Debug, Deserialize)]
pub(crate) struct QueryResult {
id: usize,
data: Value,
}
+1 -1
View File
@@ -24,7 +24,7 @@ rink = { path = "../rink" }
crossterm = "0.23.0"
tokio = { version = "1.15.0", features = ["full"] }
futures = "0.3.19"
taffy = "0.2.1"
taffy = "0.3.12"
[dev-dependencies]
dioxus = { path = "../dioxus" }
@@ -1,67 +0,0 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
use dioxus_tui::Config;
fn main() {
dioxus_tui::launch_cfg(app, Config::default());
}
#[derive(Props, PartialEq)]
struct QuadrentProps {
color: String,
text: String,
}
fn Quadrant(cx: Scope<QuadrentProps>) -> Element {
cx.render(rsx! {
div {
border_width: "1px",
width: "50%",
height: "100%",
background_color: "{cx.props.color}",
justify_content: "center",
align_items: "center",
"{cx.props.text}"
}
})
}
fn app(cx: Scope) -> Element {
cx.render(rsx! {
div {
width: "100%",
height: "100%",
flex_direction: "column",
div {
width: "100%",
height: "50%",
flex_direction: "row",
Quadrant{
color: "red".to_string(),
text: "[A]".to_string()
},
Quadrant{
color: "black".to_string(),
text: "[B]".to_string()
}
}
div {
width: "100%",
height: "50%",
flex_direction: "row",
Quadrant{
color: "green".to_string(),
text: "[C]".to_string()
},
Quadrant{
color: "blue".to_string(),
text: "[D]".to_string()
}
}
}
})
}
+39 -32
View File
@@ -1,7 +1,31 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
use dioxus_tui::Config;
fn main() {
dioxus_tui::launch(app);
dioxus_tui::launch_cfg(app, Config::default());
}
#[derive(Props, PartialEq)]
struct QuadrentProps {
color: String,
text: String,
}
fn Quadrant(cx: Scope<QuadrentProps>) -> Element {
cx.render(rsx! {
div {
border_width: "1px",
width: "50%",
height: "100%",
background_color: "{cx.props.color}",
justify_content: "center",
align_items: "center",
"{cx.props.text}"
}
})
}
fn app(cx: Scope) -> Element {
@@ -15,22 +39,13 @@ fn app(cx: Scope) -> Element {
width: "100%",
height: "50%",
flex_direction: "row",
div {
border_width: "1px",
width: "50%",
height: "100%",
background_color: "red",
justify_content: "center",
align_items: "center",
"[A]"
}
div {
width: "50%",
height: "100%",
background_color: "black",
justify_content: "center",
align_items: "center",
"[B]"
Quadrant{
color: "red".to_string(),
text: "[A]".to_string()
},
Quadrant{
color: "black".to_string(),
text: "[B]".to_string()
}
}
@@ -38,21 +53,13 @@ fn app(cx: Scope) -> Element {
width: "100%",
height: "50%",
flex_direction: "row",
div {
width: "50%",
height: "100%",
background_color: "green",
justify_content: "center",
align_items: "center",
"[C]"
}
div {
width: "50%",
height: "100%",
background_color: "blue",
justify_content: "center",
align_items: "center",
"[D]"
Quadrant{
color: "green".to_string(),
text: "[C]".to_string()
},
Quadrant{
color: "blue".to_string(),
text: "[D]".to_string()
}
}
}
+99
View File
@@ -0,0 +1,99 @@
use std::{
any::Any,
fmt::{Display, Formatter},
rc::Rc,
};
use dioxus_core::{ElementId, Mutations, VirtualDom};
use dioxus_html::{
geometry::euclid::{Point2D, Rect, Size2D},
MountedData, MountedError, RenderedElementBacking,
};
use dioxus_native_core::NodeId;
use rink::query::{ElementRef, Query};
pub(crate) fn find_mount_events(mutations: &Mutations) -> Vec<ElementId> {
let mut mount_events = Vec::new();
for mutation in &mutations.edits {
if let dioxus_core::Mutation::NewEventListener {
name: "mounted",
id,
} = mutation
{
mount_events.push(*id);
}
}
mount_events
}
// We need to queue the mounted events to give rink time to rendere and resolve the layout of elements after they are created
pub(crate) fn create_mounted_events(
vdom: &VirtualDom,
events: &mut Vec<(ElementId, &'static str, Rc<dyn Any>, bool)>,
mount_events: impl Iterator<Item = (ElementId, NodeId)>,
) {
let query: Query = vdom
.base_scope()
.consume_context()
.expect("Query should be in context");
for (id, node_id) in mount_events {
let element = TuiElement {
query: query.clone(),
id: node_id,
};
events.push((id, "mounted", Rc::new(MountedData::new(element)), false));
}
}
struct TuiElement {
query: Query,
id: NodeId,
}
impl TuiElement {
pub(crate) fn element(&self) -> ElementRef {
self.query.get(self.id)
}
}
impl RenderedElementBacking for TuiElement {
fn get_client_rect(
&self,
) -> std::pin::Pin<
Box<
dyn futures::Future<
Output = dioxus_html::MountedResult<dioxus_html::geometry::euclid::Rect<f64, f64>>,
>,
>,
> {
let layout = self.element().layout();
Box::pin(async move {
match layout {
Some(layout) => {
let x = layout.location.x as f64;
let y = layout.location.y as f64;
let width = layout.size.width as f64;
let height = layout.size.height as f64;
Ok(Rect::new(Point2D::new(x, y), Size2D::new(width, height)))
}
None => Err(MountedError::OperationFailed(Box::new(TuiElementNotFound))),
}
})
}
fn get_raw_element(&self) -> dioxus_html::MountedResult<&dyn std::any::Any> {
Ok(self)
}
}
#[derive(Debug)]
struct TuiElementNotFound;
impl Display for TuiElementNotFound {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "TUI element not found")
}
}
impl std::error::Error for TuiElementNotFound {}
+50 -9
View File
@@ -1,7 +1,9 @@
mod element;
pub mod prelude;
pub mod widgets;
use std::{
any::Any,
ops::Deref,
rc::Rc,
sync::{Arc, RwLock},
@@ -12,6 +14,7 @@ use dioxus_html::EventData;
use dioxus_native_core::dioxus::{DioxusState, NodeImmutableDioxusExt};
use dioxus_native_core::prelude::*;
use element::{create_mounted_events, find_mount_events};
pub use rink::{query::Query, Config, RenderingMode, Size, TuiContext};
use rink::{render, Driver};
@@ -37,14 +40,32 @@ pub fn launch_cfg_with_props<Props: 'static>(app: Component<Props>, props: Props
mapping: dioxus_state.clone(),
});
let muts = vdom.rebuild();
let mut rdom = rdom.write().unwrap();
dioxus_state
.write()
.unwrap()
.apply_mutations(&mut rdom, muts);
let mut queued_events = Vec::new();
{
let mut rdom = rdom.write().unwrap();
let mut dioxus_state = dioxus_state.write().unwrap();
// Find any mount events
let mounted = dbg!(find_mount_events(&muts));
dioxus_state.apply_mutations(&mut rdom, muts);
// Send the mount events
create_mounted_events(
&vdom,
&mut queued_events,
mounted
.iter()
.map(|id| (*dbg!(id), dioxus_state.element_to_node_id(*id))),
);
}
DioxusRenderer {
vdom,
dioxus_state,
queued_events,
#[cfg(all(feature = "hot-reload", debug_assertions))]
hot_reload_rx: {
let (hot_reload_tx, hot_reload_rx) =
@@ -62,6 +83,8 @@ pub fn launch_cfg_with_props<Props: 'static>(app: Component<Props>, props: Props
struct DioxusRenderer {
vdom: VirtualDom,
dioxus_state: Rc<RwLock<DioxusState>>,
// Events that are queued up to be sent to the vdom next time the vdom is polled
queued_events: Vec<(ElementId, &'static str, Rc<dyn Any>, bool)>,
#[cfg(all(feature = "hot-reload", debug_assertions))]
hot_reload_rx: tokio::sync::mpsc::UnboundedReceiver<dioxus_hot_reload::HotReloadMsg>,
}
@@ -71,10 +94,23 @@ impl Driver for DioxusRenderer {
let muts = self.vdom.render_immediate();
{
let mut rdom = rdom.write().unwrap();
self.dioxus_state
.write()
.unwrap()
.apply_mutations(&mut rdom, muts);
{
// Find any mount events
let mounted = find_mount_events(&muts);
let mut dioxus_state = self.dioxus_state.write().unwrap();
dioxus_state.apply_mutations(&mut rdom, muts);
// Send the mount events
create_mounted_events(
&self.vdom,
&mut self.queued_events,
mounted
.iter()
.map(|id| (*id, dioxus_state.element_to_node_id(*id))),
);
}
}
}
@@ -94,6 +130,11 @@ impl Driver for DioxusRenderer {
}
fn poll_async(&mut self) -> std::pin::Pin<Box<dyn futures::Future<Output = ()> + '_>> {
// Add any queued events
for (id, event, value, bubbles) in self.queued_events.drain(..) {
self.vdom.handle_event(event, value, id, bubbles);
}
#[cfg(all(feature = "hot-reload", debug_assertions))]
return Box::pin(async {
let hot_reload_wait = self.hot_reload_rx.recv();
+4
View File
@@ -34,6 +34,10 @@ use std::future::Future;
/// don't care about actions in your app being synchronized, you can use [`use_callback`]
/// hook to spawn multiple tasks and run them concurrently.
///
/// ### Notice
/// In order to use ``rx.next().await``, you will need to extend the ``Stream`` trait (used by ``UnboundedReceiver``)
/// by adding the ``futures-util`` crate as a dependency and adding ``StreamExt`` into scope via ``use futures_util::stream::StreamExt;``
///
/// ## Example
///
/// ```rust, ignore
+5
View File
@@ -41,6 +41,11 @@ features = [
"FocusEvent",
"CompositionEvent",
"ClipboardEvent",
"Element",
"DomRect",
"ScrollIntoViewOptions",
"ScrollLogicalPosition",
"ScrollBehavior",
]
[dev-dependencies]
+45 -12
View File
@@ -31,16 +31,16 @@ macro_rules! impl_attribute {
(
$(#[$attr_method:meta])*
$fil:ident: $vil:ident (in $ns:ident),
$fil:ident: $vil:ident (in $ns:literal),
) => {
pub const $fil: AttributeDiscription = (stringify!($fil), Some(stringify!($ns)), false)
pub const $fil: AttributeDiscription = (stringify!($fil), Some($ns), false)
};
(
$(#[$attr_method:meta])*
$fil:ident: $vil:ident (in $ns:ident : volatile),
$fil:ident: $vil:ident (in $ns:literal : volatile),
) => {
pub const $fil: AttributeDiscription = (stringify!($fil), Some(stringify!($ns)), true)
pub const $fil: AttributeDiscription = (stringify!($fil), Some($ns), true)
};
}
@@ -71,10 +71,10 @@ macro_rules! impl_attribute_match {
};
(
$attr:ident $fil:ident: $vil:ident (in $ns:ident),
$attr:ident $fil:ident: $vil:ident (in $ns:literal),
) => {
if $attr == stringify!($fil) {
return Some((stringify!(fil), Some(stringify!(ns))));
return Some((stringify!(fil), Some(ns)));
}
};
}
@@ -110,7 +110,7 @@ macro_rules! impl_element {
(
$(#[$attr:meta])*
$name:ident $namespace:tt {
$name:ident $namespace:literal {
$(
$(#[$attr_method:meta])*
$fil:ident: $vil:ident $extra:tt,
@@ -130,7 +130,35 @@ macro_rules! impl_element {
$(
impl_attribute!(
$(#[$attr_method])*
$fil: $vil in $namespace $extra
$fil: $vil ($extra),
);
)*
}
};
(
$(#[$attr:meta])*
$element:ident [$name:literal, $namespace:tt] {
$(
$(#[$attr_method:meta])*
$fil:ident: $vil:ident $extra:tt,
)*
}
) => {
#[allow(non_camel_case_types)]
$(#[$attr])*
pub struct $element;
impl SvgAttributes for $element {}
impl $element {
pub const TAG_NAME: &'static str = $name;
pub const NAME_SPACE: Option<&'static str> = Some($namespace);
$(
impl_attribute!(
$(#[$attr_method])*
$fil: $vil ($extra),
);
)*
}
@@ -192,7 +220,7 @@ macro_rules! impl_element_match_attributes {
if $el == stringify!($name) {
$(
impl_attribute_match!(
$attr $fil: $vil in $namespace $extra
$attr $fil: $vil ($extra),
);
)*
}
@@ -359,6 +387,11 @@ builder_constructors! {
/// element.
header None {};
/// Build a
/// [`<hgroup>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/hgroup)
/// element.
hgroup None {};
/// Build a
/// [`<h1>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h1)
/// element.
@@ -1594,7 +1627,7 @@ builder_constructors! {
// /// Build a
// /// [`<use>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/use)
// /// element.
// use "http://www.w3.org/2000/svg" {};
r#use ["use", "http://www.w3.org/2000/svg"] {
href: String DEFAULT,
};
}
+3
View File
@@ -33,6 +33,7 @@ mod form;
mod image;
mod keyboard;
mod media;
mod mounted;
mod mouse;
mod pointer;
mod scroll;
@@ -51,6 +52,7 @@ pub use form::*;
pub use image::*;
pub use keyboard::*;
pub use media::*;
pub use mounted::*;
pub use mouse::*;
pub use pointer::*;
pub use scroll::*;
@@ -144,6 +146,7 @@ pub fn event_bubbles(evt: &str) -> bool {
"animationiteration" => true,
"transitionend" => true,
"toggle" => true,
"mounted" => false,
_ => true,
}
}
+131
View File
@@ -0,0 +1,131 @@
//! Handles quering data from the renderer
use euclid::Rect;
use std::{
any::Any,
fmt::{Display, Formatter},
future::Future,
pin::Pin,
rc::Rc,
};
/// An Element that has been rendered and allows reading and modifying information about it.
///
/// Different platforms will have different implementations and different levels of support for this trait. Renderers that do not support specific features will return `None` for those queries.
// we can not use async_trait here because it does not create a trait that is object safe
pub trait RenderedElementBacking {
/// Get the renderer specific element for the given id
fn get_raw_element(&self) -> MountedResult<&dyn Any> {
Err(MountedError::NotSupported)
}
/// Get the bounding rectangle of the element relative to the viewport (this does not include the scroll position)
#[allow(clippy::type_complexity)]
fn get_client_rect(&self) -> Pin<Box<dyn Future<Output = MountedResult<Rect<f64, f64>>>>> {
Box::pin(async { Err(MountedError::NotSupported) })
}
/// Scroll to make the element visible
fn scroll_to(
&self,
_behavior: ScrollBehavior,
) -> Pin<Box<dyn Future<Output = MountedResult<()>>>> {
Box::pin(async { Err(MountedError::NotSupported) })
}
/// Set the focus on the element
fn set_focus(&self, _focus: bool) -> Pin<Box<dyn Future<Output = MountedResult<()>>>> {
Box::pin(async { Err(MountedError::NotSupported) })
}
}
impl RenderedElementBacking for () {}
/// The way that scrolling should be performed
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
pub enum ScrollBehavior {
/// Scroll to the element immediately
#[cfg_attr(feature = "serialize", serde(rename = "instant"))]
Instant,
/// Scroll to the element smoothly
#[cfg_attr(feature = "serialize", serde(rename = "smooth"))]
Smooth,
}
/// An Element that has been rendered and allows reading and modifying information about it.
///
/// Different platforms will have different implementations and different levels of support for this trait. Renderers that do not support specific features will return `None` for those queries.
pub struct MountedData {
inner: Rc<dyn RenderedElementBacking>,
}
impl MountedData {
/// Create a new MountedData
pub fn new(registry: impl RenderedElementBacking + 'static) -> Self {
Self {
inner: Rc::new(registry),
}
}
/// Get the renderer specific element for the given id
pub fn get_raw_element(&self) -> MountedResult<&dyn Any> {
self.inner.get_raw_element()
}
/// Get the bounding rectangle of the element relative to the viewport (this does not include the scroll position)
pub async fn get_client_rect(&self) -> MountedResult<Rect<f64, f64>> {
self.inner.get_client_rect().await
}
/// Scroll to make the element visible
pub fn scroll_to(
&self,
behavior: ScrollBehavior,
) -> Pin<Box<dyn Future<Output = MountedResult<()>>>> {
self.inner.scroll_to(behavior)
}
/// Set the focus on the element
pub fn set_focus(&self, focus: bool) -> Pin<Box<dyn Future<Output = MountedResult<()>>>> {
self.inner.set_focus(focus)
}
}
use dioxus_core::Event;
pub type MountedEvent = Event<MountedData>;
impl_event! [
MountedData;
/// mounted
onmounted
];
/// The MountedResult type for the MountedData
pub type MountedResult<T> = Result<T, MountedError>;
#[derive(Debug)]
/// The error type for the MountedData
pub enum MountedError {
/// The renderer does not support the requested operation
NotSupported,
/// The element was not found
OperationFailed(Box<dyn std::error::Error>),
}
impl Display for MountedError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
MountedError::NotSupported => {
write!(f, "The renderer does not support the requested operation")
}
MountedError::OperationFailed(e) => {
write!(f, "The operation failed: {}", e)
}
}
}
}
impl std::error::Error for MountedError {}
+5
View File
@@ -113,6 +113,9 @@ fn fun_name(
// Toggle
"toggle" => Toggle(de(data)?),
// Mounted
"mounted" => Mounted,
// ImageData => "load" | "error";
// OtherData => "abort" | "afterprint" | "beforeprint" | "beforeunload" | "hashchange" | "languagechange" | "message" | "offline" | "online" | "pagehide" | "pageshow" | "popstate" | "rejectionhandled" | "storage" | "unhandledrejection" | "unload" | "userproximity" | "vrdisplayactivate" | "vrdisplayblur" | "vrdisplayconnect" | "vrdisplaydeactivate" | "vrdisplaydisconnect" | "vrdisplayfocus" | "vrdisplaypointerrestricted" | "vrdisplaypointerunrestricted" | "vrdisplaypresentchange";
other => {
@@ -151,6 +154,7 @@ pub enum EventData {
Animation(AnimationData),
Transition(TransitionData),
Toggle(ToggleData),
Mounted,
}
impl EventData {
@@ -172,6 +176,7 @@ impl EventData {
EventData::Animation(data) => Rc::new(data) as Rc<dyn Any>,
EventData::Transition(data) => Rc::new(data) as Rc<dyn Any>,
EventData::Toggle(data) => Rc::new(data) as Rc<dyn Any>,
EventData::Mounted => Rc::new(MountedData::new(())) as Rc<dyn Any>,
}
}
}
+69 -4
View File
@@ -4,14 +4,18 @@ use crate::events::{
};
use crate::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint};
use crate::input_data::{decode_key_location, decode_mouse_button_set, MouseButton};
use crate::DragData;
use crate::{
DragData, MountedData, MountedError, MountedResult, RenderedElementBacking, ScrollBehavior,
};
use keyboard_types::{Code, Key, Modifiers};
use std::convert::TryInto;
use std::future::Future;
use std::pin::Pin;
use std::str::FromStr;
use wasm_bindgen::JsCast;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{
AnimationEvent, CompositionEvent, Event, KeyboardEvent, MouseEvent, PointerEvent, TouchEvent,
TransitionEvent, WheelEvent,
AnimationEvent, CompositionEvent, Event, KeyboardEvent, MouseEvent, PointerEvent,
ScrollIntoViewOptions, TouchEvent, TransitionEvent, WheelEvent,
};
macro_rules! uncheck_convert {
@@ -193,3 +197,64 @@ impl From<&TransitionEvent> for TransitionData {
}
}
}
impl From<&web_sys::Element> for MountedData {
fn from(e: &web_sys::Element) -> Self {
MountedData::new(e.clone())
}
}
impl RenderedElementBacking for web_sys::Element {
fn get_client_rect(
&self,
) -> Pin<Box<dyn Future<Output = MountedResult<euclid::Rect<f64, f64>>>>> {
let rect = self.get_bounding_client_rect();
let result = Ok(euclid::Rect::new(
euclid::Point2D::new(rect.left(), rect.top()),
euclid::Size2D::new(rect.width(), rect.height()),
));
Box::pin(async { result })
}
fn get_raw_element(&self) -> MountedResult<&dyn std::any::Any> {
Ok(self)
}
fn scroll_to(
&self,
behavior: ScrollBehavior,
) -> Pin<Box<dyn Future<Output = MountedResult<()>>>> {
match behavior {
ScrollBehavior::Instant => self.scroll_into_view_with_scroll_into_view_options(
ScrollIntoViewOptions::new().behavior(web_sys::ScrollBehavior::Instant),
),
ScrollBehavior::Smooth => self.scroll_into_view_with_scroll_into_view_options(
ScrollIntoViewOptions::new().behavior(web_sys::ScrollBehavior::Smooth),
),
}
Box::pin(async { Ok(()) })
}
fn set_focus(&self, focus: bool) -> Pin<Box<dyn Future<Output = MountedResult<()>>>> {
let result = self
.dyn_ref::<web_sys::HtmlElement>()
.ok_or_else(|| MountedError::OperationFailed(Box::new(FocusError(self.into()))))
.and_then(|e| {
(if focus { e.focus() } else { e.blur() })
.map_err(|err| MountedError::OperationFailed(Box::new(FocusError(err))))
});
Box::pin(async { result })
}
}
#[derive(Debug)]
struct FocusError(JsValue);
impl std::fmt::Display for FocusError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "failed to focus element {:?}", self.0)
}
}
impl std::error::Error for FocusError {}
+2
View File
@@ -18,8 +18,10 @@ js-sys = { version = "0.3.56", optional = true }
web-sys = { version = "0.3.56", optional = true, features = ["Element", "Node"] }
sledgehammer_bindgen = { version = "0.2.1", optional = true }
sledgehammer_utils = { version = "0.1.1", optional = true }
serde = { version = "1.0", features = ["derive"], optional = true }
[features]
default = []
serialize = ["serde"]
web = ["wasm-bindgen", "js-sys", "web-sys"]
sledgehammer = ["wasm-bindgen", "js-sys", "web-sys", "sledgehammer_bindgen", "sledgehammer_utils"]
+56 -3
View File
@@ -204,6 +204,45 @@ class Interpreter {
node.removeAttribute(name);
}
}
GetClientRect(id) {
const node = this.nodes[id];
if (!node) {
return;
}
const rect = node.getBoundingClientRect();
return {
type: "GetClientRect",
origin: [rect.x, rect.y],
size: [rect.width, rect.height],
};
}
ScrollTo(id, behavior) {
const node = this.nodes[id];
if (!node) {
return false;
}
node.scrollIntoView({
behavior: behavior,
});
return true;
}
/// Set the focus on the element
SetFocus(id, focus) {
const node = this.nodes[id];
if (!node) {
return false;
}
if (focus) {
node.focus();
} else {
node.blur();
}
return true;
}
handleEdits(edits) {
for (let template of edits.templates) {
this.SaveTemplate(template);
@@ -346,9 +385,21 @@ class Interpreter {
case "NewEventListener":
let bubbles = event_bubbles(edit.name);
this.NewEventListener(edit.name, edit.id, bubbles, (event) => {
handler(event, edit.name, bubbles);
});
// if this is a mounted listener, we send the event immediately
if (edit.name === "mounted") {
window.ipc.postMessage(
serializeIpcMessage("user_event", {
name: edit.name,
element: edit.id,
data: null,
bubbles,
})
);
} else {
this.NewEventListener(edit.name, edit.id, bubbles, (event) => {
handler(event, edit.name, bubbles);
});
}
break;
}
}
@@ -933,6 +984,8 @@ function event_bubbles(event) {
return true;
case "toggle":
return true;
case "mounted":
return false;
}
return true;
@@ -117,6 +117,9 @@ mod js {
export function set_node(id, node) {
nodes[id] = node;
}
export function get_node(id) {
return nodes[id];
}
export function initilize(root, handler) {
listeners.handler = handler;
nodes = [root];
@@ -167,6 +170,9 @@ mod js {
#[wasm_bindgen]
pub fn set_node(id: u32, node: Node);
#[wasm_bindgen]
pub fn get_node(id: u32) -> Node;
#[wasm_bindgen]
pub fn initilize(root: Node, handler: &Function);
}
+2
View File
@@ -14,6 +14,8 @@ license = "MIT/Apache-2.0"
[dependencies]
thiserror = "1.0.38"
log = "0.4.14"
slab = "0.4"
futures-util = { version = "0.3.25", default-features = false, features = [
"sink",
] }
+125
View File
@@ -0,0 +1,125 @@
use dioxus_core::ElementId;
use dioxus_html::{geometry::euclid::Rect, MountedResult, RenderedElementBacking};
use tokio::sync::mpsc::UnboundedSender;
use crate::query::QueryEngine;
/// A mounted element passed to onmounted events
pub struct LiveviewElement {
id: ElementId,
query_tx: UnboundedSender<String>,
query: QueryEngine,
}
impl LiveviewElement {
pub(crate) fn new(id: ElementId, tx: UnboundedSender<String>, query: QueryEngine) -> Self {
Self {
id,
query_tx: tx,
query,
}
}
}
impl RenderedElementBacking for LiveviewElement {
fn get_raw_element(&self) -> dioxus_html::MountedResult<&dyn std::any::Any> {
Ok(self)
}
fn get_client_rect(
&self,
) -> std::pin::Pin<
Box<
dyn futures_util::Future<
Output = dioxus_html::MountedResult<dioxus_html::geometry::euclid::Rect<f64, f64>>,
>,
>,
> {
let script = format!("return window.interpreter.GetClientRect({});", self.id.0);
let fut = self
.query
.new_query::<Option<Rect<f64, f64>>>(&script, &self.query_tx)
.resolve();
Box::pin(async move {
match fut.await {
Ok(Some(rect)) => Ok(rect),
Ok(None) => MountedResult::Err(dioxus_html::MountedError::OperationFailed(
Box::new(DesktopQueryError::FailedToQuery),
)),
Err(err) => {
MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err)))
}
}
})
}
fn scroll_to(
&self,
behavior: dioxus_html::ScrollBehavior,
) -> std::pin::Pin<Box<dyn futures_util::Future<Output = dioxus_html::MountedResult<()>>>> {
let script = format!(
"return window.interpreter.ScrollTo({}, {});",
self.id.0,
serde_json::to_string(&behavior).expect("Failed to serialize ScrollBehavior")
);
let fut = self
.query
.new_query::<bool>(&script, &self.query_tx)
.resolve();
Box::pin(async move {
match fut.await {
Ok(true) => Ok(()),
Ok(false) => MountedResult::Err(dioxus_html::MountedError::OperationFailed(
Box::new(DesktopQueryError::FailedToQuery),
)),
Err(err) => {
MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err)))
}
}
})
}
fn set_focus(
&self,
focus: bool,
) -> std::pin::Pin<Box<dyn futures_util::Future<Output = dioxus_html::MountedResult<()>>>> {
let script = format!(
"return window.interpreter.SetFocus({}, {});",
self.id.0, focus
);
let fut = self
.query
.new_query::<bool>(&script, &self.query_tx)
.resolve();
Box::pin(async move {
match fut.await {
Ok(true) => Ok(()),
Ok(false) => MountedResult::Err(dioxus_html::MountedError::OperationFailed(
Box::new(DesktopQueryError::FailedToQuery),
)),
Err(err) => {
MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err)))
}
}
})
}
}
#[derive(Debug)]
enum DesktopQueryError {
FailedToQuery,
}
impl std::fmt::Display for DesktopQueryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DesktopQueryError::FailedToQuery => write!(f, "Failed to query the element"),
}
}
}
impl std::error::Error for DesktopQueryError {}
+2
View File
@@ -18,7 +18,9 @@ pub mod adapters {
pub use adapters::*;
mod element;
pub mod pool;
mod query;
use futures_util::{SinkExt, StreamExt};
pub use pool::*;
+12 -4
View File
@@ -26,11 +26,19 @@ class IPC {
// todo: retry the connection
};
ws.onmessage = (event) => {
ws.onmessage = (message) => {
// Ignore pongs
if (event.data != "__pong__") {
let edits = JSON.parse(event.data);
window.interpreter.handleEdits(edits);
if (message.data != "__pong__") {
const event = JSON.parse(message.data);
switch (event.type) {
case "edits":
let edits = event.data;
window.interpreter.handleEdits(edits);
break;
case "query":
Function("Eval", `"use strict";${event.data};`)();
break;
}
}
};
+64 -12
View File
@@ -1,8 +1,13 @@
use crate::LiveViewError;
use dioxus_core::prelude::*;
use dioxus_html::HtmlEvent;
use crate::{
element::LiveviewElement,
query::{QueryEngine, QueryResult},
LiveViewError,
};
use dioxus_core::{prelude::*, Mutations};
use dioxus_html::{EventData, HtmlEvent, MountedData};
use futures_util::{pin_mut, SinkExt, StreamExt};
use std::time::Duration;
use serde::Serialize;
use std::{rc::Rc, time::Duration};
use tokio_util::task::LocalPoolHandle;
#[derive(Clone)]
@@ -115,7 +120,7 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
};
// todo: use an efficient binary packed format for this
let edits = serde_json::to_string(&vdom.rebuild()).unwrap();
let edits = serde_json::to_string(&ClientUpdate::Edits(vdom.rebuild())).unwrap();
// pin the futures so we can use select!
pin_mut!(ws);
@@ -123,11 +128,19 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
// send the initial render to the client
ws.send(edits).await?;
// Create the a proxy for query engine
let (query_tx, mut query_rx) = tokio::sync::mpsc::unbounded_channel();
let query_engine = QueryEngine::default();
// desktop uses this wrapper struct thing around the actual event itself
// this is sorta driven by tao/wry
#[derive(serde::Deserialize)]
struct IpcMessage {
params: HtmlEvent,
#[derive(serde::Deserialize, Debug)]
#[serde(tag = "method", content = "params")]
enum IpcMessage {
#[serde(rename = "user_event")]
Event(HtmlEvent),
#[serde(rename = "query")]
Query(QueryResult),
}
loop {
@@ -147,16 +160,45 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
ws.send("__pong__".to_string()).await?;
}
Some(Ok(evt)) => {
if let Ok(IpcMessage { params }) = serde_json::from_str::<IpcMessage>(evt) {
vdom.handle_event(&params.name, params.data.into_any(), params.element, params.bubbles);
if let Ok(message) = serde_json::from_str::<IpcMessage>(evt) {
match message {
IpcMessage::Event(evt) => {
// Intercept the mounted event and insert a custom element type
if let EventData::Mounted = &evt.data {
let element = LiveviewElement::new(evt.element, query_tx.clone(), query_engine.clone());
vdom.handle_event(
&evt.name,
Rc::new(MountedData::new(element)),
evt.element,
evt.bubbles,
);
}
else{
vdom.handle_event(
&evt.name,
evt.data.into_any(),
evt.element,
evt.bubbles,
);
}
}
IpcMessage::Query(result) => {
query_engine.send(result);
},
}
}
}
// log this I guess? when would we get an error here?
Some(Err(_e)) => {},
Some(Err(_e)) => {}
None => return Ok(()),
}
}
// handle any new queries
Some(query) = query_rx.recv() => {
ws.send(serde_json::to_string(&ClientUpdate::Query(query)).unwrap()).await?;
}
Some(msg) = hot_reload_wait => {
#[cfg(all(feature = "hot-reload", debug_assertions))]
match msg{
@@ -176,6 +218,16 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
.render_with_deadline(tokio::time::sleep(Duration::from_millis(10)))
.await;
ws.send(serde_json::to_string(&edits).unwrap()).await?;
ws.send(serde_json::to_string(&ClientUpdate::Edits(edits)).unwrap())
.await?;
}
}
#[derive(Serialize)]
#[serde(tag = "type", content = "data")]
enum ClientUpdate<'a> {
#[serde(rename = "edits")]
Edits(Mutations<'a>),
#[serde(rename = "query")]
Query(String),
}
+113
View File
@@ -0,0 +1,113 @@
use std::{cell::RefCell, rc::Rc};
use serde::{de::DeserializeOwned, Deserialize};
use serde_json::Value;
use slab::Slab;
use thiserror::Error;
use tokio::sync::{broadcast::error::RecvError, mpsc::UnboundedSender};
/// Tracks what query ids are currently active
#[derive(Default, Clone)]
struct SharedSlab {
slab: Rc<RefCell<Slab<()>>>,
}
/// Handles sending and receiving arbitrary queries from the webview. Queries can be resolved non-sequentially, so we use ids to track them.
#[derive(Clone)]
pub(crate) struct QueryEngine {
sender: Rc<tokio::sync::broadcast::Sender<QueryResult>>,
active_requests: SharedSlab,
}
impl Default for QueryEngine {
fn default() -> Self {
let (sender, _) = tokio::sync::broadcast::channel(8);
Self {
sender: Rc::new(sender),
active_requests: SharedSlab::default(),
}
}
}
impl QueryEngine {
/// Creates a new query and returns a handle to it. The query will be resolved when the webview returns a result with the same id.
pub fn new_query<V: DeserializeOwned>(
&self,
script: &str,
tx: &UnboundedSender<String>,
) -> Query<V> {
let request_id = self.active_requests.slab.borrow_mut().insert(());
// start the query
// We embed the return of the eval in a function so we can send it back to the main thread
if let Err(err) = tx.send(format!(
r#"window.ipc.postMessage(
JSON.stringify({{
"method":"query",
"params": {{
"id": {request_id},
"data": (function(){{{script}}})()
}}
}})
);"#
)) {
log::warn!("Query error: {err}");
}
Query {
slab: self.active_requests.clone(),
id: request_id,
reciever: self.sender.subscribe(),
phantom: std::marker::PhantomData,
}
}
/// Send a query result
pub fn send(&self, data: QueryResult) {
let _ = self.sender.send(data);
}
}
pub(crate) struct Query<V: DeserializeOwned> {
slab: SharedSlab,
id: usize,
reciever: tokio::sync::broadcast::Receiver<QueryResult>,
phantom: std::marker::PhantomData<V>,
}
impl<V: DeserializeOwned> Query<V> {
/// Resolve the query
pub async fn resolve(mut self) -> Result<V, QueryError> {
let result = loop {
match self.reciever.recv().await {
Ok(result) => {
if result.id == self.id {
break V::deserialize(result.data).map_err(QueryError::DeserializeError);
}
}
Err(err) => {
break Err(QueryError::RecvError(err));
}
}
};
// Remove the query from the slab
self.slab.slab.borrow_mut().remove(self.id);
result
}
}
#[derive(Error, Debug)]
pub enum QueryError {
#[error("Error receiving query result: {0}")]
RecvError(RecvError),
#[error("Error deserializing query result: {0}")]
DeserializeError(serde_json::Error),
}
#[derive(Clone, Debug, Deserialize)]
pub(crate) struct QueryResult {
id: usize,
data: Value,
}
+1 -1
View File
@@ -14,7 +14,7 @@ keywords = ["dom", "ui", "gui", "react"]
dioxus-core = { path = "../core", version = "^0.3.0", optional = true }
keyboard-types = "0.6.2"
taffy = "0.2.1"
taffy = "0.3.12"
smallvec = "1.6"
rustc-hash = "1.1.0"
anymap = "1.0.0-beta.2"
+292 -113
View File
@@ -2,7 +2,7 @@
/*
- [ ] pub display: Display, ----> taffy doesnt support all display types
- [x] pub position_type: PositionType, --> taffy doesnt support everything
- [x] pub position: Position, --> taffy doesnt support everything
- [x] pub direction: Direction,
- [x] pub flex_direction: FlexDirection,
@@ -11,6 +11,14 @@
- [x] pub flex_shrink: f32,
- [x] pub flex_basis: Dimension,
- [x]pub grid_auto_flow: GridAutoFlow,
- [x]pub grid_template_rows: GridTrackVec<TrackSizingFunction>,
- [x]pub grid_template_columns: GridTrackVec<TrackSizingFunction>,
- [x]pub grid_auto_rows: GridTrackVec<NonRepeatedTrackSizingFunction>,
- [x]pub grid_auto_columns: GridTrackVec<NonRepeatedTrackSizingFunction>,
- [x]pub grid_row: Line<GridPlacement>,
- [x]pub grid_column: Line<GridPlacement>,
- [x] pub overflow: Overflow, ---> taffy doesnt have support for directional overflow
- [x] pub align_items: AlignItems,
@@ -21,20 +29,22 @@
- [x] pub padding: Rect<Dimension>,
- [x] pub justify_content: JustifyContent,
- [x] pub position: Rect<Dimension>,
- [x] pub inset: Rect<Dimension>,
- [x] pub border: Rect<Dimension>,
- [ ] pub size: Size<Dimension>, ----> seems to only be relevant for input?
- [ ] pub min_size: Size<Dimension>,
- [ ] pub max_size: Size<Dimension>,
- [ ] pub aspect_ratio: Number, ----> parsing is done, but taffy doesnt support it
- [x] pub aspect_ratio: Number,
*/
use lightningcss::properties::border::LineStyle;
use lightningcss::properties::{align, display, flex, position, size};
use lightningcss::properties::grid::{TrackBreadth, TrackSizing};
use lightningcss::properties::{align, border, display, flex, grid, position, size};
use lightningcss::values::percentage::Percentage;
use lightningcss::{
properties::{align::GapValue, border::BorderSideWidth, Property, PropertyId},
properties::{Property, PropertyId},
stylesheet::ParserOptions,
traits::Parse,
values::{
@@ -45,7 +55,7 @@ use lightningcss::{
};
use taffy::{
prelude::*,
style::{FlexDirection, PositionType},
style::{FlexDirection, Position},
};
/// Default values for layout attributes
@@ -95,33 +105,35 @@ pub fn apply_layout_attributes_cfg(
display::Display::Keyword(display::DisplayKeyword::None) => {
style.display = Display::None
}
display::Display::Pair(pair) => {
if let display::DisplayInside::Flex(_) = pair.inside {
style.display = Display::Flex
display::Display::Pair(pair) => match pair.inside {
display::DisplayInside::Flex(_) => {
style.display = Display::Flex;
}
}
_ => (),
display::DisplayInside::Grid => {
style.display = Display::Grid;
}
_ => {}
},
_ => {}
},
Property::Position(position) => {
style.position_type = match position {
position::Position::Relative => PositionType::Relative,
position::Position::Absolute => PositionType::Absolute,
style.position = match position {
position::Position::Relative => Position::Relative,
position::Position::Absolute => Position::Absolute,
_ => return,
}
}
Property::Top(top) => style.position.top = convert_length_percentage_or_auto(top),
Property::Top(top) => style.inset.top = convert_length_percentage_or_auto(top),
Property::Bottom(bottom) => {
style.position.bottom = convert_length_percentage_or_auto(bottom)
}
Property::Left(left) => style.position.left = convert_length_percentage_or_auto(left),
Property::Right(right) => {
style.position.right = convert_length_percentage_or_auto(right)
style.inset.bottom = convert_length_percentage_or_auto(bottom)
}
Property::Left(left) => style.inset.left = convert_length_percentage_or_auto(left),
Property::Right(right) => style.inset.right = convert_length_percentage_or_auto(right),
Property::Inset(inset) => {
style.position.top = convert_length_percentage_or_auto(inset.top);
style.position.bottom = convert_length_percentage_or_auto(inset.bottom);
style.position.left = convert_length_percentage_or_auto(inset.left);
style.position.right = convert_length_percentage_or_auto(inset.right);
style.inset.top = convert_length_percentage_or_auto(inset.top);
style.inset.bottom = convert_length_percentage_or_auto(inset.bottom);
style.inset.left = convert_length_percentage_or_auto(inset.left);
style.inset.right = convert_length_percentage_or_auto(inset.right);
}
Property::BorderTopWidth(width) => {
style.border.top = convert_border_side_width(width, &config.border_widths);
@@ -164,46 +176,64 @@ pub fn apply_layout_attributes_cfg(
}
Property::BorderTopStyle(line_style) => {
if line_style != LineStyle::None {
style.border.top =
convert_border_side_width(BorderSideWidth::Medium, &config.border_widths);
style.border.top = convert_border_side_width(
border::BorderSideWidth::Medium,
&config.border_widths,
);
}
}
Property::BorderBottomStyle(line_style) => {
if line_style != LineStyle::None {
style.border.bottom =
convert_border_side_width(BorderSideWidth::Medium, &config.border_widths);
style.border.bottom = convert_border_side_width(
border::BorderSideWidth::Medium,
&config.border_widths,
);
}
}
Property::BorderLeftStyle(line_style) => {
if line_style != LineStyle::None {
style.border.left =
convert_border_side_width(BorderSideWidth::Medium, &config.border_widths);
style.border.left = convert_border_side_width(
border::BorderSideWidth::Medium,
&config.border_widths,
);
}
}
Property::BorderRightStyle(line_style) => {
if line_style != LineStyle::None {
style.border.right =
convert_border_side_width(BorderSideWidth::Medium, &config.border_widths);
style.border.right = convert_border_side_width(
border::BorderSideWidth::Medium,
&config.border_widths,
);
}
}
Property::BorderStyle(styles) => {
if styles.top != LineStyle::None {
style.border.top =
convert_border_side_width(BorderSideWidth::Medium, &config.border_widths);
style.border.top = convert_border_side_width(
border::BorderSideWidth::Medium,
&config.border_widths,
);
}
if styles.bottom != LineStyle::None {
style.border.bottom =
convert_border_side_width(BorderSideWidth::Medium, &config.border_widths);
style.border.bottom = convert_border_side_width(
border::BorderSideWidth::Medium,
&config.border_widths,
);
}
if styles.left != LineStyle::None {
style.border.left =
convert_border_side_width(BorderSideWidth::Medium, &config.border_widths);
style.border.left = convert_border_side_width(
border::BorderSideWidth::Medium,
&config.border_widths,
);
}
if styles.right != LineStyle::None {
style.border.right =
convert_border_side_width(BorderSideWidth::Medium, &config.border_widths);
style.border.right = convert_border_side_width(
border::BorderSideWidth::Medium,
&config.border_widths,
);
}
}
// Flexbox properties
Property::FlexDirection(flex_direction, _) => {
use FlexDirection::*;
style.flex_direction = match flex_direction {
@@ -228,75 +258,123 @@ pub fn apply_layout_attributes_cfg(
style.flex_shrink = shrink;
}
Property::FlexBasis(basis, _) => {
style.flex_basis = convert_length_percentage_or_auto(basis);
style.flex_basis = convert_length_percentage_or_auto(basis).into();
}
Property::Flex(flex, _) => {
style.flex_grow = flex.grow;
style.flex_shrink = flex.shrink;
style.flex_basis = convert_length_percentage_or_auto(flex.basis);
style.flex_basis = convert_length_percentage_or_auto(flex.basis).into();
}
// Grid properties
Property::GridAutoFlow(grid_auto_flow) => {
let is_row = grid_auto_flow.contains(grid::GridAutoFlow::Row);
let is_dense = grid_auto_flow.contains(grid::GridAutoFlow::Dense);
style.grid_auto_flow = match (is_row, is_dense) {
(true, false) => GridAutoFlow::Row,
(false, false) => GridAutoFlow::Column,
(true, true) => GridAutoFlow::RowDense,
(false, true) => GridAutoFlow::ColumnDense,
};
}
Property::GridTemplateColumns(TrackSizing::TrackList(track_list)) => {
style.grid_template_columns = track_list
.items
.into_iter()
.map(convert_grid_track_item)
.collect();
}
Property::GridTemplateRows(TrackSizing::TrackList(track_list)) => {
style.grid_template_rows = track_list
.items
.into_iter()
.map(convert_grid_track_item)
.collect();
}
Property::GridAutoColumns(grid::TrackSizeList(track_size_list)) => {
style.grid_auto_columns = track_size_list
.into_iter()
.map(convert_grid_track_size)
.collect();
}
Property::GridAutoRows(grid::TrackSizeList(track_size_list)) => {
style.grid_auto_rows = track_size_list
.into_iter()
.map(convert_grid_track_size)
.collect();
}
Property::GridRow(grid_row) => {
style.grid_row = Line {
start: convert_grid_placement(grid_row.start),
end: convert_grid_placement(grid_row.end),
};
}
Property::GridColumn(grid_column) => {
style.grid_column = Line {
start: convert_grid_placement(grid_column.start),
end: convert_grid_placement(grid_column.end),
};
}
// Alignment properties
Property::AlignContent(align, _) => {
use AlignContent::*;
style.align_content = match align {
align::AlignContent::ContentDistribution(distribution) => match distribution {
align::ContentDistribution::SpaceBetween => SpaceBetween,
align::ContentDistribution::SpaceAround => SpaceAround,
align::ContentDistribution::SpaceEvenly => SpaceEvenly,
align::ContentDistribution::Stretch => Stretch,
align::ContentDistribution::SpaceBetween => Some(SpaceBetween),
align::ContentDistribution::SpaceAround => Some(SpaceAround),
align::ContentDistribution::SpaceEvenly => Some(SpaceEvenly),
align::ContentDistribution::Stretch => Some(Stretch),
},
align::AlignContent::ContentPosition {
value: position, ..
} => match position {
align::ContentPosition::Center => Center,
align::ContentPosition::Start | align::ContentPosition::FlexStart => {
FlexStart
}
align::ContentPosition::End | align::ContentPosition::FlexEnd => FlexEnd,
align::ContentPosition::Center => Some(Center),
align::ContentPosition::Start => Some(Start),
align::ContentPosition::FlexStart => Some(FlexStart),
align::ContentPosition::End => Some(End),
align::ContentPosition::FlexEnd => Some(FlexEnd),
},
_ => return,
};
}
Property::JustifyContent(justify, _) => {
use JustifyContent::*;
use AlignContent::*;
style.justify_content = match justify {
align::JustifyContent::ContentDistribution(distribution) => {
match distribution {
align::ContentDistribution::SpaceBetween => SpaceBetween,
align::ContentDistribution::SpaceAround => SpaceAround,
align::ContentDistribution::SpaceEvenly => SpaceEvenly,
align::ContentDistribution::SpaceBetween => Some(SpaceBetween),
align::ContentDistribution::SpaceAround => Some(SpaceAround),
align::ContentDistribution::SpaceEvenly => Some(SpaceEvenly),
_ => return,
}
}
align::JustifyContent::ContentPosition {
value: position, ..
} => match position {
align::ContentPosition::Center => Center,
// start ignores -reverse flex-direction but there is no way to specify that in Taffy
align::ContentPosition::Start | align::ContentPosition::FlexStart => {
FlexStart
}
// end ignores -reverse flex-direction but there is no way to specify that in Taffy
align::ContentPosition::End | align::ContentPosition::FlexEnd => FlexEnd,
align::ContentPosition::Center => Some(Center),
align::ContentPosition::Start => Some(Start),
align::ContentPosition::FlexStart => Some(FlexStart),
align::ContentPosition::End => Some(End),
align::ContentPosition::FlexEnd => Some(FlexEnd),
},
_ => return,
};
}
Property::AlignSelf(align, _) => {
use AlignSelf::*;
use AlignItems::*;
style.align_self = match align {
align::AlignSelf::Auto => Auto,
align::AlignSelf::Stretch => Stretch,
align::AlignSelf::BaselinePosition(_) => Baseline,
align::AlignSelf::Auto => None,
align::AlignSelf::Stretch => Some(Stretch),
align::AlignSelf::BaselinePosition(_) => Some(Baseline),
align::AlignSelf::SelfPosition {
value: position, ..
} => match position {
align::SelfPosition::Center => Center,
align::SelfPosition::Start
| align::SelfPosition::SelfStart
| align::SelfPosition::FlexStart => FlexStart,
align::SelfPosition::End
| align::SelfPosition::SelfEnd
| align::SelfPosition::FlexEnd => FlexEnd,
align::SelfPosition::Center => Some(Center),
align::SelfPosition::Start | align::SelfPosition::SelfStart => Some(Start),
align::SelfPosition::FlexStart => Some(FlexStart),
align::SelfPosition::End | align::SelfPosition::SelfEnd => Some(End),
align::SelfPosition::FlexEnd => Some(FlexEnd),
},
_ => return,
};
@@ -304,15 +382,18 @@ pub fn apply_layout_attributes_cfg(
Property::AlignItems(align, _) => {
use AlignItems::*;
style.align_items = match align {
align::AlignItems::BaselinePosition(_) => Baseline,
align::AlignItems::Stretch => Stretch,
align::AlignItems::BaselinePosition(_) => Some(Baseline),
align::AlignItems::Stretch => Some(Stretch),
align::AlignItems::SelfPosition {
value: position, ..
} => match position {
align::SelfPosition::Center => Center,
align::SelfPosition::FlexStart => FlexStart,
align::SelfPosition::FlexEnd => FlexEnd,
_ => return,
align::SelfPosition::Center => Some(Center),
align::SelfPosition::FlexStart => Some(FlexStart),
align::SelfPosition::FlexEnd => Some(FlexEnd),
align::SelfPosition::Start | align::SelfPosition::SelfStart => {
Some(FlexEnd)
}
align::SelfPosition::End | align::SelfPosition::SelfEnd => Some(FlexEnd),
},
_ => return,
};
@@ -350,23 +431,23 @@ pub fn apply_layout_attributes_cfg(
};
}
Property::PaddingTop(padding) => {
style.padding.top = convert_length_percentage_or_auto(padding);
style.padding.top = convert_padding(padding);
}
Property::PaddingBottom(padding) => {
style.padding.bottom = convert_length_percentage_or_auto(padding);
style.padding.bottom = convert_padding(padding);
}
Property::PaddingLeft(padding) => {
style.padding.left = convert_length_percentage_or_auto(padding);
style.padding.left = convert_padding(padding);
}
Property::PaddingRight(padding) => {
style.padding.right = convert_length_percentage_or_auto(padding);
style.padding.right = convert_padding(padding);
}
Property::Padding(padding) => {
style.padding = Rect {
top: convert_length_percentage_or_auto(padding.top),
bottom: convert_length_percentage_or_auto(padding.bottom),
left: convert_length_percentage_or_auto(padding.left),
right: convert_length_percentage_or_auto(padding.right),
top: convert_padding(padding.top),
bottom: convert_padding(padding.bottom),
left: convert_padding(padding.left),
right: convert_padding(padding.right),
};
}
Property::Width(width) => {
@@ -386,59 +467,157 @@ pub fn apply_layout_attributes_cfg(
}
}
fn convert_length_value(length_value: LengthValue) -> Dimension {
fn extract_px_value(length_value: LengthValue) -> f32 {
match length_value {
LengthValue::Px(value) => Dimension::Points(value),
LengthValue::Px(value) => value,
_ => todo!(),
}
}
fn convert_dimension_percentage(
fn convert_length_percentage(
dimension_percentage: DimensionPercentage<LengthValue>,
) -> Dimension {
) -> LengthPercentage {
match dimension_percentage {
DimensionPercentage::Dimension(value) => convert_length_value(value),
DimensionPercentage::Percentage(percentage) => Dimension::Percent(percentage.0),
_ => todo!(),
DimensionPercentage::Dimension(value) => LengthPercentage::Points(extract_px_value(value)),
DimensionPercentage::Percentage(percentage) => LengthPercentage::Percent(percentage.0),
DimensionPercentage::Calc(_) => todo!(),
}
}
fn convert_padding(dimension_percentage: LengthPercentageOrAuto) -> LengthPercentage {
match dimension_percentage {
LengthPercentageOrAuto::Auto => unimplemented!(),
LengthPercentageOrAuto::LengthPercentage(lp) => match lp {
DimensionPercentage::Dimension(value) => {
LengthPercentage::Points(extract_px_value(value))
}
DimensionPercentage::Percentage(percentage) => LengthPercentage::Percent(percentage.0),
DimensionPercentage::Calc(_) => unimplemented!(),
},
}
}
fn convert_length_percentage_or_auto(
length_percentage_or_auto: LengthPercentageOrAuto,
) -> Dimension {
match length_percentage_or_auto {
LengthPercentageOrAuto::Auto => Dimension::Auto,
LengthPercentageOrAuto::LengthPercentage(percentage) => {
convert_dimension_percentage(percentage)
}
dimension_percentage: LengthPercentageOrAuto,
) -> LengthPercentageAuto {
match dimension_percentage {
LengthPercentageOrAuto::Auto => LengthPercentageAuto::Auto,
LengthPercentageOrAuto::LengthPercentage(lp) => match lp {
DimensionPercentage::Dimension(value) => {
LengthPercentageAuto::Points(extract_px_value(value))
}
DimensionPercentage::Percentage(percentage) => {
LengthPercentageAuto::Percent(percentage.0)
}
DimensionPercentage::Calc(_) => todo!(),
},
}
}
fn convert_dimension(dimension_percentage: DimensionPercentage<LengthValue>) -> Dimension {
match dimension_percentage {
DimensionPercentage::Dimension(value) => Dimension::Points(extract_px_value(value)),
DimensionPercentage::Percentage(percentage) => Dimension::Percent(percentage.0),
DimensionPercentage::Calc(_) => todo!(),
}
}
fn convert_border_side_width(
border_side_width: BorderSideWidth,
border_side_width: border::BorderSideWidth,
border_width_config: &BorderWidths,
) -> Dimension {
) -> LengthPercentage {
match border_side_width {
BorderSideWidth::Length(Length::Value(value)) => convert_length_value(value),
BorderSideWidth::Thick => Dimension::Points(border_width_config.thick),
BorderSideWidth::Medium => Dimension::Points(border_width_config.medium),
BorderSideWidth::Thin => Dimension::Points(border_width_config.thin),
_ => todo!(),
border::BorderSideWidth::Length(Length::Value(value)) => {
LengthPercentage::Points(extract_px_value(value))
}
border::BorderSideWidth::Thick => LengthPercentage::Points(border_width_config.thick),
border::BorderSideWidth::Medium => LengthPercentage::Points(border_width_config.medium),
border::BorderSideWidth::Thin => LengthPercentage::Points(border_width_config.thin),
border::BorderSideWidth::Length(_) => unimplemented!(),
}
}
fn convert_gap_value(gap_value: GapValue) -> Dimension {
fn convert_gap_value(gap_value: align::GapValue) -> LengthPercentage {
match gap_value {
GapValue::LengthPercentage(dim) => convert_dimension_percentage(dim),
GapValue::Normal => Dimension::Auto,
align::GapValue::LengthPercentage(dim) => convert_length_percentage(dim),
align::GapValue::Normal => LengthPercentage::Points(0.0),
}
}
fn convert_size(size: size::Size) -> Dimension {
match size {
size::Size::Auto => Dimension::Auto,
size::Size::LengthPercentage(length) => convert_dimension_percentage(length),
_ => todo!(),
size::Size::LengthPercentage(length) => convert_dimension(length),
size::Size::MinContent(_) => Dimension::Auto, // Unimplemented, so default auto
size::Size::MaxContent(_) => Dimension::Auto, // Unimplemented, so default auto
size::Size::FitContent(_) => Dimension::Auto, // Unimplemented, so default auto
size::Size::FitContentFunction(_) => Dimension::Auto, // Unimplemented, so default auto
size::Size::Stretch(_) => Dimension::Auto, // Unimplemented, so default auto
size::Size::Contain => Dimension::Auto, // Unimplemented, so default auto
}
}
fn convert_grid_placement(input: grid::GridLine) -> GridPlacement {
match input {
grid::GridLine::Auto => GridPlacement::Auto,
grid::GridLine::Line { index, .. } => line(index as i16),
grid::GridLine::Span { index, .. } => span(index as u16),
grid::GridLine::Area { .. } => unimplemented!(),
}
}
fn convert_grid_track_item(input: grid::TrackListItem) -> TrackSizingFunction {
match input {
grid::TrackListItem::TrackSize(size) => {
TrackSizingFunction::Single(convert_grid_track_size(size))
}
grid::TrackListItem::TrackRepeat(_) => todo!(), // TODO: requires TrackRepeat fields to be public!
}
}
fn convert_grid_track_size(input: grid::TrackSize) -> NonRepeatedTrackSizingFunction {
match input {
grid::TrackSize::TrackBreadth(breadth) => minmax(
convert_track_breadth_min(&breadth),
convert_track_breadth_max(&breadth),
),
grid::TrackSize::MinMax { min, max } => minmax(
convert_track_breadth_min(&min),
convert_track_breadth_max(&max),
),
grid::TrackSize::FitContent(limit) => match limit {
DimensionPercentage::Dimension(LengthValue::Px(len)) => minmax(auto(), points(len)),
DimensionPercentage::Percentage(Percentage(pct)) => minmax(auto(), percent(pct)),
_ => unimplemented!(),
},
}
}
fn convert_track_breadth_max(breadth: &TrackBreadth) -> MaxTrackSizingFunction {
match breadth {
grid::TrackBreadth::Length(length_percentage) => match length_percentage {
DimensionPercentage::Dimension(LengthValue::Px(len)) => points(*len),
DimensionPercentage::Percentage(Percentage(pct)) => percent(*pct),
_ => unimplemented!(),
},
grid::TrackBreadth::Flex(fraction) => fr(*fraction),
grid::TrackBreadth::MinContent => MaxTrackSizingFunction::MinContent,
grid::TrackBreadth::MaxContent => MaxTrackSizingFunction::MaxContent,
grid::TrackBreadth::Auto => MaxTrackSizingFunction::Auto,
}
}
fn convert_track_breadth_min(breadth: &TrackBreadth) -> MinTrackSizingFunction {
match breadth {
grid::TrackBreadth::Length(length_percentage) => match length_percentage {
DimensionPercentage::Dimension(LengthValue::Px(len)) => points(*len),
DimensionPercentage::Percentage(Percentage(pct)) => percent(*pct),
_ => unimplemented!(),
},
grid::TrackBreadth::MinContent => MinTrackSizingFunction::MinContent,
grid::TrackBreadth::MaxContent => MinTrackSizingFunction::MaxContent,
grid::TrackBreadth::Auto => MinTrackSizingFunction::Auto,
grid::TrackBreadth::Flex(_) => MinTrackSizingFunction::Auto,
}
}
+1 -1
View File
@@ -178,7 +178,7 @@ impl<'a> TreeMut for TreeMutView<'a> {
fn set_height(tree: &mut TreeMutView<'_>, node: NodeId, height: u16) {
let children = {
let mut node_data_mut = &mut tree.1;
let mut node = (&mut node_data_mut).get(node).unwrap();
let node = (&mut node_data_mut).get(node).unwrap();
node.height = height;
node.children.clone()
};
+1 -1
View File
@@ -22,7 +22,7 @@ crossterm = "0.23.0"
anyhow = "1.0.42"
tokio = { version = "1.15.0", features = ["full"] }
futures = "0.3.19"
taffy = "0.2.1"
taffy = "0.3.12"
smallvec = "1.6"
rustc-hash = "1.1.0"
anymap = "1.0.0-beta.2"
File renamed without changes.
+56 -34
View File
@@ -81,10 +81,11 @@ impl State for TaffyLayout {
};
if let PossiblyUninitalized::Initialized(n) = self.node {
if self.style != style {
taffy.set_style(n, style).unwrap();
taffy.set_style(n, style.clone()).unwrap();
}
} else {
self.node = PossiblyUninitalized::Initialized(taffy.new_leaf(style).unwrap());
self.node =
PossiblyUninitalized::Initialized(taffy.new_leaf(style.clone()).unwrap());
changed = true;
}
} else {
@@ -117,69 +118,90 @@ impl State for TaffyLayout {
child_layout.push(l.node.unwrap());
}
fn scale_dimention(d: Dimension) -> Dimension {
fn scale_dimension(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),
fn scale_length_percentage_auto(d: LengthPercentageAuto) -> LengthPercentageAuto {
match d {
LengthPercentageAuto::Points(p) => {
LengthPercentageAuto::Points(unit_to_layout_space(p))
}
LengthPercentageAuto::Percent(p) => LengthPercentageAuto::Percent(p),
LengthPercentageAuto::Auto => LengthPercentageAuto::Auto,
}
}
fn scale_length_percentage(d: LengthPercentage) -> LengthPercentage {
match d {
LengthPercentage::Points(p) => {
LengthPercentage::Points(unit_to_layout_space(p))
}
LengthPercentage::Percent(p) => LengthPercentage::Percent(p),
}
}
let scaled_style = Style {
inset: Rect {
left: scale_length_percentage_auto(style.inset.left),
right: scale_length_percentage_auto(style.inset.right),
top: scale_length_percentage_auto(style.inset.top),
bottom: scale_length_percentage_auto(style.inset.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),
left: scale_length_percentage_auto(style.margin.left),
right: scale_length_percentage_auto(style.margin.right),
top: scale_length_percentage_auto(style.margin.top),
bottom: scale_length_percentage_auto(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),
left: scale_length_percentage(style.padding.left),
right: scale_length_percentage(style.padding.right),
top: scale_length_percentage(style.padding.top),
bottom: scale_length_percentage(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),
left: scale_length_percentage(style.border.left),
right: scale_length_percentage(style.border.right),
top: scale_length_percentage(style.border.top),
bottom: scale_length_percentage(style.border.bottom),
},
gap: Size {
width: scale_dimention(style.gap.width),
height: scale_dimention(style.gap.height),
width: scale_length_percentage(style.gap.width),
height: scale_length_percentage(style.gap.height),
},
flex_basis: scale_dimention(style.flex_basis),
flex_basis: scale_dimension(style.flex_basis),
size: Size {
width: scale_dimention(style.size.width),
height: scale_dimention(style.size.height),
width: scale_dimension(style.size.width),
height: scale_dimension(style.size.height),
},
min_size: Size {
width: scale_dimention(style.min_size.width),
height: scale_dimention(style.min_size.height),
width: scale_dimension(style.min_size.width),
height: scale_dimension(style.min_size.height),
},
max_size: Size {
width: scale_dimention(style.max_size.width),
height: scale_dimention(style.max_size.height),
width: scale_dimension(style.max_size.width),
height: scale_dimension(style.max_size.height),
},
..style
..style.clone()
};
if let PossiblyUninitalized::Initialized(n) = self.node {
if self.style != style {
taffy.set_style(n, style).unwrap();
taffy.set_style(n, scaled_style).unwrap();
}
if taffy.children(n).unwrap() != child_layout {
taffy.set_children(n, &child_layout).unwrap();
}
} else {
self.node = PossiblyUninitalized::Initialized(
taffy.new_with_children(style, &child_layout).unwrap(),
taffy
.new_with_children(scaled_style, &child_layout)
.unwrap(),
);
changed = true;
}
+1 -1
View File
@@ -172,7 +172,7 @@ pub fn render<R: Driver>(
.unwrap();
// the root node fills the entire area
let mut style = *taffy.style(root_node).unwrap();
let mut style = taffy.style(root_node).unwrap().clone();
let new_size = Size {
width: Dimension::Points(width),
height: Dimension::Points(height),
+1 -9
View File
@@ -91,15 +91,7 @@ impl<'a> ElementRef<'a> {
pub fn layout(&self) -> Option<Layout> {
let layout = self
.stretch
.layout(
self.inner
.get(self.id)
.unwrap()
.get::<TaffyLayout>()
.unwrap()
.node
.ok()?,
)
.layout(self.inner.get(self.id)?.get::<TaffyLayout>()?.node.ok()?)
.ok();
layout.map(|layout| Layout {
order: layout.order,
+1 -1
View File
@@ -26,7 +26,7 @@ impl<'a> RinkBuffer<'a> {
// panic!("({x}, {y}) is not in {area:?}");
return;
}
let mut cell = self.buf.get_mut(x, y);
let cell = self.buf.get_mut(x, y);
cell.bg = convert(self.cfg.rendering_mode, new.bg.blend(cell.bg));
if new.symbol.is_empty() {
if !cell.symbol.is_empty() {
File renamed without changes.
+68 -23
View File
@@ -10,8 +10,8 @@
use dioxus_core::{
BorrowedAttributeValue, ElementId, Mutation, Template, TemplateAttribute, TemplateNode,
};
use dioxus_html::{event_bubbles, CompositionData, FileEngine, FormData};
use dioxus_interpreter_js::{save_template, Channel};
use dioxus_html::{event_bubbles, CompositionData, FileEngine, FormData, MountedData};
use dioxus_interpreter_js::{get_node, save_template, Channel};
use futures_channel::mpsc;
use js_sys::Array;
use rustc_hash::FxHashMap;
@@ -28,6 +28,7 @@ pub struct WebsysDom {
templates: FxHashMap<String, u32>,
max_template_id: u32,
pub(crate) interpreter: Channel,
event_channel: mpsc::UnboundedSender<UiEvent>,
}
pub struct UiEvent {
@@ -35,7 +36,6 @@ pub struct UiEvent {
pub bubbles: bool,
pub element: ElementId,
pub data: Rc<dyn Any>,
pub event: Event,
}
impl WebsysDom {
@@ -49,8 +49,9 @@ impl WebsysDom {
};
let interpreter = Channel::default();
let handler: Closure<dyn FnMut(&Event)> =
Closure::wrap(Box::new(move |event: &web_sys::Event| {
let handler: Closure<dyn FnMut(&Event)> = Closure::wrap(Box::new({
let event_channel = event_channel.clone();
move |event: &web_sys::Event| {
let name = event.type_();
let element = walk_event_for_id(event);
let bubbles = dioxus_html::event_bubbles(name.as_str());
@@ -74,10 +75,10 @@ impl WebsysDom {
bubbles,
element,
data,
event: event.clone(),
});
}
}));
}
}));
dioxus_interpreter_js::initilize(
root.clone().unchecked_into(),
@@ -90,6 +91,7 @@ impl WebsysDom {
interpreter,
templates: FxHashMap::default(),
max_template_id: 0,
event_channel,
}
}
@@ -161,6 +163,8 @@ impl WebsysDom {
pub fn apply_edits(&mut self, mut edits: Vec<Mutation>) {
use Mutation::*;
let i = &mut self.interpreter;
// we need to apply the mount events last, so we collect them here
let mut to_mount = Vec::new();
for edit in &edits {
match edit {
AppendChildren { id, m } => i.append_children(id.0 as u32, *m as u32),
@@ -211,18 +215,43 @@ impl WebsysDom {
},
SetText { value, id } => i.set_text(id.0 as u32, value),
NewEventListener { name, id, .. } => {
console::log_1(&format!("new event listener: {}", name).into());
i.new_event_listener(name, id.0 as u32, event_bubbles(name) as u8);
}
RemoveEventListener { name, id } => {
i.remove_event_listener(name, id.0 as u32, event_bubbles(name) as u8)
match *name {
// mounted events are fired immediately after the element is mounted.
"mounted" => {
to_mount.push(*id);
}
_ => {
i.new_event_listener(name, id.0 as u32, event_bubbles(name) as u8);
}
}
}
RemoveEventListener { name, id } => match *name {
"mounted" => {}
_ => {
i.remove_event_listener(name, id.0 as u32, event_bubbles(name) as u8);
}
},
Remove { id } => i.remove(id.0 as u32),
PushRoot { id } => i.push_root(id.0 as u32),
}
}
edits.clear();
i.flush();
for id in to_mount {
let node = get_node(id.0 as u32);
if let Some(element) = node.dyn_ref::<Element>() {
log::info!("mounted event fired: {}", id.0);
let data: MountedData = element.into();
let data = Rc::new(data);
let _ = self.event_channel.unbounded_send(UiEvent {
name: "mounted".to_string(),
bubbles: false,
element: id,
data,
});
}
}
}
}
@@ -380,22 +409,38 @@ extern "C" {
}
fn walk_event_for_id(event: &web_sys::Event) -> Option<(ElementId, web_sys::Element)> {
let mut target = event
let target = event
.target()
.expect("missing target")
.dyn_into::<web_sys::Element>()
.expect("not a valid element");
.dyn_into::<web_sys::Node>()
.expect("not a valid node");
let mut current_target_element = target.dyn_ref::<web_sys::Element>().cloned();
loop {
match target.get_attribute("data-dioxus-id").map(|f| f.parse()) {
Some(Ok(id)) => return Some((ElementId(id), target)),
Some(Err(_)) => return None,
match (
current_target_element
.as_ref()
.and_then(|el| el.get_attribute("data-dioxus-id").map(|f| f.parse())),
current_target_element,
) {
// This node is an element, and has a dioxus id, so we can stop walking
(Some(Ok(id)), Some(target)) => return Some((ElementId(id), target)),
// walk the tree upwards until we actually find an event target
None => match target.parent_element() {
Some(parent) => target = parent,
None => return None,
},
// Walk the tree upwards until we actually find an event target
(None, target_element) => {
let parent = match target_element.as_ref() {
Some(el) => el.parent_element(),
// if this is the first node and not an element, we need to get the parent from the target node
None => target.parent_element(),
};
match parent {
Some(parent) => current_target_element = Some(parent),
_ => return None,
}
}
// This node is an element with an invalid dioxus id, give up
_ => return None,
}
}
}