Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: More reliable devtools #667

Merged
merged 6 commits into from
Jun 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/devtools/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ freya-core = { workspace = true, features = ["shared"] }
freya-components = { workspace = true }
freya-engine = { workspace = true }
torin = { workspace = true }
dioxus-radio = "0.2"

dioxus = { workspace = true }
freya-native-core = { workspace = true }
Expand Down
4 changes: 2 additions & 2 deletions crates/devtools/src/hooks/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
mod use_selected_node;
mod use_node_info;

pub use use_selected_node::*;
pub use use_node_info::*;
12 changes: 12 additions & 0 deletions crates/devtools/src/hooks/use_node_info.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use crate::state::DevtoolsChannel;
use dioxus_radio::prelude::use_radio;
use freya_native_core::prelude::NodeId;
use freya_renderer::devtools::NodeInfo;

pub fn use_node_info(node_id: NodeId) -> Option<NodeInfo> {
let radio = use_radio(DevtoolsChannel::UpdatedDOM);
let state = radio.read();
let nodes = state.devtools_receiver.borrow();

nodes.iter().find(|node| node.id == node_id).cloned()
}
13 changes: 0 additions & 13 deletions crates/devtools/src/hooks/use_selected_node.rs

This file was deleted.

216 changes: 83 additions & 133 deletions crates/devtools/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,18 @@
use dioxus::prelude::*;
use dioxus_radio::prelude::*;
use dioxus_router::prelude::*;
use freya_components::*;
use freya_core::{
dom::SafeDOM,
node::{get_node_state, NodeState},
};
use freya_elements::elements as dioxus_elements;
use freya_hooks::{use_init_theme, use_theme, DARK_THEME};
use freya_native_core::node::NodeType;
use freya_native_core::prelude::ElementNode;
use freya_native_core::real_dom::NodeImmutable;
use freya_hooks::{use_init_theme, use_platform, DARK_THEME};
use freya_native_core::NodeId;

use freya_renderer::HoveredNode;
use std::sync::Arc;
use tokio::sync::Notify;
use torin::prelude::LayoutNode;
use freya_renderer::{devtools::DevtoolsReceiver, HoveredNode};
use state::{DevtoolsChannel, DevtoolsState};

mod hooks;
mod node;
mod property;
mod state;
mod tab;
mod tabs;

Expand All @@ -28,17 +21,15 @@ use tabs::{layout::*, style::*, tree::*};

/// Run the [`VirtualDom`] with a sidepanel where the devtools are located.
pub fn with_devtools(
rdom: SafeDOM,
root: fn() -> Element,
mutations_notifier: Arc<Notify>,
devtools_receiver: DevtoolsReceiver,
hovered_node: HoveredNode,
) -> VirtualDom {
VirtualDom::new_with_props(
AppWithDevtools,
AppWithDevtoolsProps {
root,
rdom,
mutations_notifier,
devtools_receiver,
hovered_node,
},
)
Expand All @@ -47,8 +38,7 @@ pub fn with_devtools(
#[derive(Props, Clone)]
struct AppWithDevtoolsProps {
root: fn() -> Element,
rdom: SafeDOM,
mutations_notifier: Arc<Notify>,
devtools_receiver: DevtoolsReceiver,
hovered_node: HoveredNode,
}

Expand All @@ -62,8 +52,8 @@ impl PartialEq for AppWithDevtoolsProps {
fn AppWithDevtools(props: AppWithDevtoolsProps) -> Element {
#[allow(non_snake_case)]
let Root = props.root;
let mutations_notifier = props.mutations_notifier.clone();
let hovered_node = props.hovered_node.clone();
let devtools_receiver = props.devtools_receiver;
let hovered_node = props.hovered_node;

rsx!(
NativeContainer {
Expand All @@ -83,9 +73,8 @@ fn AppWithDevtools(props: AppWithDevtoolsProps) -> Element {
width: "350",
ThemeProvider {
DevTools {
rdom: props.rdom.clone(),
mutations_notifier: mutations_notifier,
hovered_node: hovered_node
devtools_receiver,
hovered_node
}
}
}
Expand All @@ -94,21 +83,9 @@ fn AppWithDevtools(props: AppWithDevtoolsProps) -> Element {
)
}

#[derive(Clone, PartialEq)]
pub struct TreeNode {
tag: String,
id: NodeId,
height: u16,
#[allow(dead_code)]
text: Option<String>,
state: NodeState,
layout_node: LayoutNode,
}

#[derive(Props, Clone)]
pub struct DevToolsProps {
rdom: SafeDOM,
mutations_notifier: Arc<Notify>,
devtools_receiver: DevtoolsReceiver,
hovered_node: HoveredNode,
}

Expand All @@ -120,76 +97,15 @@ impl PartialEq for DevToolsProps {

#[allow(non_snake_case)]
pub fn DevTools(props: DevToolsProps) -> Element {
let mut children = use_context_provider(|| Signal::new(Vec::<TreeNode>::new()));
use_context_provider::<Signal<HoveredNode>>(|| Signal::new(props.hovered_node.clone()));
use_init_theme(|| DARK_THEME);
let theme = use_theme();
let theme = use_init_theme(|| DARK_THEME);
use_init_radio_station::<DevtoolsState, DevtoolsChannel>(|| DevtoolsState {
hovered_node: props.hovered_node.clone(),
devtools_receiver: props.devtools_receiver.clone(),
});

let theme = theme.read();
let color = &theme.body.color;

use_hook(move || {
spawn(async move {
let DevToolsProps {
mutations_notifier,
rdom,
..
} = props;
loop {
mutations_notifier.notified().await;

let dom = rdom.try_get();
if let Some(dom) = dom {
let rdom = dom.rdom();
let layout = dom.layout();

let mut new_children = Vec::with_capacity(rdom.tree_ref().len());

let mut root_found = false;
let mut devtools_found = false;

rdom.traverse_depth_first(|node| {
let height = node.height();
if height == 3 {
if !root_found {
root_found = true;
} else {
devtools_found = true;
}
}

if !devtools_found && root_found {
let layout_node = layout.get(node.id());
if let Some(layout_node) = layout_node {
let (text, tag) = match &*node.node_type() {
NodeType::Text(text) => {
(Some(text.to_string()), "text".to_string())
}
NodeType::Element(ElementNode { tag, .. }) => {
(None, tag.to_string())
}
NodeType::Placeholder => (None, "placeholder".to_string()),
};

let state = get_node_state(&node);

new_children.push(TreeNode {
height,
id: node.id(),
tag,
text,
state,
layout_node: layout_node.clone(),
});
}
}
});
*children.write() = new_children;
}
}
});
});

rsx!(
rect {
width: "fill",
Expand All @@ -214,36 +130,21 @@ pub fn DevtoolsBar() -> Element {
)
}

#[allow(non_snake_case)]
#[component]
pub fn NodeInspectorBar(node_id: NodeId) -> Element {
rsx!(
TabsBar {
TabButton {
to: Route::NodeInspectorStyle { node_id: node_id.serialize() },
label: "Style"
}
TabButton {
to: Route::NodeInspectorLayout { node_id: node_id.serialize() },
label: "Layout"
}
}
)
}

#[derive(Routable, Clone, PartialEq)]
#[rustfmt::skip]
pub enum Route {
#[layout(DevtoolsBar)]
#[nest("/")]
#[layout(DOMInspectorLayout)]
#[layout(LayoutForDOMInspector)]
#[route("/")]
DOMInspector {},
#[nest("/node/:node_id")]
#[route("/style")]
NodeInspectorStyle { node_id: String },
#[route("/layout")]
NodeInspectorLayout { node_id: String },
#[layout(LayoutForNodeInspector)]
#[route("/style")]
NodeInspectorStyle { node_id: String },
#[route("/layout")]
NodeInspectorLayout { node_id: String },
#[end_layout]
#[end_nest]
#[end_layout]
#[end_nest]
Expand All @@ -252,6 +153,17 @@ pub enum Route {
PageNotFound { },
}

impl Route {
pub fn get_node_id(&self) -> Option<NodeId> {
match self {
Self::NodeInspectorStyle { node_id } | Self::NodeInspectorLayout { node_id } => {
Some(NodeId::deserialize(node_id))
}
_ => None,
}
}
}

#[allow(non_snake_case)]
#[component]
fn PageNotFound() -> Element {
Expand All @@ -264,14 +176,50 @@ fn PageNotFound() -> Element {

#[allow(non_snake_case)]
#[component]
fn DOMInspectorLayout() -> Element {
let hovered_node = use_context::<Signal<HoveredNode>>();
fn LayoutForNodeInspector(node_id: String) -> Element {
rsx!(
rect {
overflow: "clip",
width: "100%",
height: "50%",
TabsBar {
TabButton {
to: Route::NodeInspectorStyle { node_id: node_id.clone() },
label: "Style"
}
TabButton {
to: Route::NodeInspectorLayout { node_id },
label: "Layout"
}
}
Outlet::<Route> {}
}
)
}

#[allow(non_snake_case)]
#[component]
fn LayoutForDOMInspector() -> Element {
let route = use_route::<Route>();
let platform = use_platform();
let mut radio = use_radio(DevtoolsChannel::Global);
use_hook(move || {
spawn(async move {
let mut devtools_receiver = radio.read().devtools_receiver.clone();
loop {
devtools_receiver
.changed()
.await
.expect("Failed while waiting for DOM changes.");

radio.write_channel(DevtoolsChannel::UpdatedDOM);
}
});
});

let selected_node_id = route.get_node_id();

let is_expanded_vertical = matches!(
route,
Route::NodeInspectorStyle { .. } | Route::NodeInspectorLayout { .. }
);
let is_expanded_vertical = selected_node_id.is_some();

let height = if is_expanded_vertical {
"calc(50% - 35)"
Expand All @@ -282,9 +230,11 @@ fn DOMInspectorLayout() -> Element {
rsx!(
NodesTree {
height,
onselected: move |node: TreeNode| {
if let Some(hovered_node) = &hovered_node.read().as_ref() {
hovered_node.lock().unwrap().replace(node.id);
selected_node_id,
onselected: move |node_id: NodeId| {
if let Some(hovered_node) = &radio.read().hovered_node.as_ref() {
hovered_node.lock().unwrap().replace(node_id);
platform.request_animation_frame();
}
}
}
Expand Down
Loading