use std::collections::HashSet;
use dioxus::prelude::*;
use dioxus_radio::prelude::*;
use dioxus_router::prelude::{
use_route,
Outlet,
Routable,
Router,
};
use freya_components::*;
use freya_elements::elements as dioxus_elements;
use freya_hooks::{
use_init_theme,
use_platform,
DARK_THEME,
};
use freya_native_core::NodeId;
use freya_renderer::{
devtools::DevtoolsReceiver,
HoveredNode,
};
use state::{
DevtoolsChannel,
DevtoolsState,
};
mod hooks;
mod node;
mod property;
mod state;
mod tabs;
use tabs::{
layout::*,
style::*,
tree::*,
};
pub fn with_devtools(
root: fn() -> Element,
devtools_receiver: DevtoolsReceiver,
hovered_node: HoveredNode,
) -> VirtualDom {
VirtualDom::new_with_props(
AppWithDevtools,
AppWithDevtoolsProps {
root,
devtools_receiver,
hovered_node,
},
)
}
#[derive(Props, Clone)]
struct AppWithDevtoolsProps {
root: fn() -> Element,
devtools_receiver: DevtoolsReceiver,
hovered_node: HoveredNode,
}
impl PartialEq for AppWithDevtoolsProps {
fn eq(&self, _other: &Self) -> bool {
true
}
}
#[allow(non_snake_case)]
fn AppWithDevtools(props: AppWithDevtoolsProps) -> Element {
#[allow(non_snake_case)]
let Root = props.root;
let devtools_receiver = props.devtools_receiver;
let hovered_node = props.hovered_node;
rsx!(
NativeContainer {
rect {
width: "100%",
height: "100%",
direction: "horizontal",
rect {
overflow: "clip",
height: "100%",
width: "calc(100% - 350)",
Root { },
}
rect {
background: "rgb(40, 40, 40)",
height: "100%",
width: "350",
ThemeProvider {
DevTools {
devtools_receiver,
hovered_node
}
}
}
}
}
)
}
#[derive(Props, Clone)]
pub struct DevToolsProps {
devtools_receiver: DevtoolsReceiver,
hovered_node: HoveredNode,
}
impl PartialEq for DevToolsProps {
fn eq(&self, _: &Self) -> bool {
true
}
}
#[allow(non_snake_case)]
pub fn DevTools(props: DevToolsProps) -> Element {
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(),
devtools_tree: HashSet::default(),
});
let theme = theme.read();
let color = &theme.body.color;
rsx!(
rect {
width: "fill",
height: "fill",
color: "{color}",
Router::<Route> { }
}
)
}
#[component]
#[allow(non_snake_case)]
pub fn DevtoolsBar() -> Element {
rsx!(
Tabsbar {
Link {
to: Route::DOMInspector { },
ActivableRoute {
route: Route::DOMInspector { },
Tab {
label {
"Elements"
}
}
}
}
}
Outlet::<Route> {}
)
}
#[derive(Routable, Clone, PartialEq)]
#[rustfmt::skip]
pub enum Route {
#[layout(DevtoolsBar)]
#[nest("/")]
#[layout(LayoutForDOMInspector)]
#[route("/")]
DOMInspector {},
#[nest("/node/:node_id")]
#[layout(LayoutForNodeInspector)]
#[route("/style")]
NodeInspectorStyle { node_id: String },
#[route("/layout")]
NodeInspectorLayout { node_id: String },
#[end_layout]
#[end_nest]
#[end_layout]
#[end_nest]
#[end_layout]
#[route("/..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 {
rsx!(
label {
"Page not found."
}
)
}
#[allow(non_snake_case)]
#[component]
fn LayoutForNodeInspector(node_id: String) -> Element {
rsx!(
rect {
overflow: "clip",
width: "100%",
height: "50%",
Tabsbar {
Link {
to: Route::NodeInspectorStyle { node_id: node_id.clone() },
ActivableRoute {
route: Route::NodeInspectorStyle { node_id: node_id.clone() },
Tab {
label {
"Style"
}
}
}
}
Link {
to: Route::NodeInspectorLayout { node_id: node_id.clone() },
ActivableRoute {
route: Route::NodeInspectorLayout { node_id },
Tab {
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 = selected_node_id.is_some();
let height = if is_expanded_vertical {
"calc(50% - 35)"
} else {
"fill"
};
rsx!(
NodesTree {
height,
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();
}
}
}
Outlet::<Route> {}
)
}
#[allow(non_snake_case)]
#[component]
fn DOMInspector() -> Element {
None
}
pub trait NodeIdSerializer {
fn serialize(&self) -> String;
fn deserialize(node_id: &str) -> Self;
}
impl NodeIdSerializer for NodeId {
fn serialize(&self) -> String {
format!("{}-{}", self.index(), self.gen())
}
fn deserialize(node_id: &str) -> Self {
let (index, gen) = node_id.split_once('-').unwrap();
NodeId::new_from_index_and_gen(index.parse().unwrap(), gen.parse().unwrap())
}
}