mod audit;
mod elements;
mod logs;
mod network;
mod probe;
mod shell;
mod sources;
mod state;
mod storage;
mod styles;
mod timelines;
pub use elements::ElementsSidebar;
pub use probe::{set_enabled, tree, with_tree, Probe, ProbeNode, ProbeTree, Probed, ProbedAny};
pub use state::*;
pub use styles::{box_model, declarations, hex, BoxModel, Declaration};
use gpui::prelude::*;
use gpui::{div, px, App, Context, EventEmitter, FocusHandle, Focusable, SharedString, Window};
use audit::AuditPanel;
use elements::ElementsPanel;
use logs::LogsPanel;
use network::NetworkPanel;
use probe::ProbeTree as Tree;
use shell::{glyph, hairline, tool_button, Ink, BAR_HEIGHT, LABEL_SIZE};
use sources::SourcesPanel;
use storage::StoragePanel;
use timelines::TimelinesPanel;
use crate::icon::IconName;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum DevToolsTab {
#[default]
Elements,
Network,
Sources,
Timelines,
Storage,
Layers,
Logs,
Audit,
}
impl DevToolsTab {
pub fn label(self) -> &'static str {
match self {
DevToolsTab::Elements => "Elements",
DevToolsTab::Network => "Network",
DevToolsTab::Sources => "Sources",
DevToolsTab::Timelines => "Timelines",
DevToolsTab::Storage => "Storage",
DevToolsTab::Layers => "Layers",
DevToolsTab::Logs => "Logs",
DevToolsTab::Audit => "Audit",
}
}
fn icon(self) -> IconName {
match self {
DevToolsTab::Elements => IconName::Code,
DevToolsTab::Network => IconName::Network,
DevToolsTab::Sources => IconName::FileCode,
DevToolsTab::Timelines => IconName::Activity,
DevToolsTab::Storage => IconName::Database,
DevToolsTab::Layers => IconName::Layers,
DevToolsTab::Logs => IconName::ScrollText,
DevToolsTab::Audit => IconName::ShieldCheck,
}
}
pub const ALL: [DevToolsTab; 8] = [
DevToolsTab::Elements,
DevToolsTab::Network,
DevToolsTab::Sources,
DevToolsTab::Timelines,
DevToolsTab::Storage,
DevToolsTab::Layers,
DevToolsTab::Logs,
DevToolsTab::Audit,
];
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Dock {
#[default]
Right,
Bottom,
Detached,
}
#[derive(Debug, Clone)]
pub enum DevToolsEvent {
Close,
Dock(Dock),
RevealSource(SourceRef),
Picking(bool),
}
pub struct DevTools {
focus: FocusHandle,
tab: DevToolsTab,
dock: Dock,
tree: Tree,
drawer_open: bool,
picking: bool,
pub(crate) elements: ElementsPanel,
pub(crate) logs: LogsPanel,
pub(crate) network: NetworkPanel,
pub(crate) storage: StoragePanel,
pub(crate) timelines: TimelinesPanel,
pub(crate) sources: SourcesPanel,
pub(crate) audit: AuditPanel,
}
impl EventEmitter<DevToolsEvent> for DevTools {}
impl Focusable for DevTools {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus.clone()
}
}
impl DevTools {
pub fn new(cx: &mut Context<Self>) -> Self {
probe::retain();
DevTools {
focus: cx.focus_handle(),
tab: DevToolsTab::default(),
dock: Dock::default(),
tree: Tree::default(),
drawer_open: false,
picking: false,
elements: ElementsPanel::default(),
logs: LogsPanel::new(cx),
network: NetworkPanel::new(cx),
storage: StoragePanel::default(),
timelines: TimelinesPanel::default(),
sources: SourcesPanel::default(),
audit: AuditPanel::default(),
}
}
pub fn tab(mut self, tab: DevToolsTab) -> Self {
self.tab = tab;
self
}
pub fn dock(mut self, dock: Dock) -> Self {
self.dock = dock;
self
}
pub fn elements_sidebar(mut self, sidebar: ElementsSidebar) -> Self {
self.elements.sidebar = sidebar;
self
}
pub fn drawer(mut self, open: bool) -> Self {
self.drawer_open = open;
self
}
pub fn active_tab(&self) -> DevToolsTab {
self.tab
}
pub fn set_tab(&mut self, tab: DevToolsTab, cx: &mut Context<Self>) {
self.tab = tab;
cx.notify();
}
pub fn is_picking(&self) -> bool {
self.picking
}
pub fn set_picking(&mut self, picking: bool, cx: &mut Context<Self>) {
self.picking = picking;
cx.emit(DevToolsEvent::Picking(picking));
cx.notify();
}
pub fn pick_at(&mut self, point: gpui::Point<gpui::Pixels>, cx: &mut Context<Self>) -> bool {
let Some(index) = self.tree.hit(point) else {
return false;
};
let key = self.tree.nodes[index].key.clone();
self.elements.reveal(&self.tree, &key);
self.tab = DevToolsTab::Elements;
self.picking = false;
cx.emit(DevToolsEvent::Picking(false));
cx.notify();
true
}
pub fn selected_bounds(&self) -> Option<gpui::Bounds<gpui::Pixels>> {
self.elements
.selected_node(&self.tree)
.map(|node| node.bounds)
}
pub fn tree(&self) -> &ProbeTree {
&self.tree
}
pub fn reveal_source(&mut self, source: SourceRef, cx: &mut Context<Self>) {
self.sources.reveal(source.clone());
self.tab = DevToolsTab::Sources;
cx.emit(DevToolsEvent::RevealSource(source));
cx.notify();
}
pub fn toggle_drawer(&mut self, cx: &mut Context<Self>) {
self.drawer_open = !self.drawer_open;
cx.notify();
}
fn toolbar(&self, ink: &Ink, cx: &mut Context<Self>) -> gpui::Div {
let (warnings, errors) = cx
.try_global::<DevToolsState>()
.map(|state| state.log_issues())
.unwrap_or((0, 0));
let (requests, transfer, _) = cx
.try_global::<DevToolsState>()
.map(|state| state.network_totals())
.unwrap_or((0, 0, 0));
let badge = |count: usize, icon: IconName, color: gpui::Hsla, cx: &App| {
div()
.flex()
.items_center()
.gap(px(3.0))
.child(glyph(icon, 11.0, color, cx))
.child(
div()
.text_color(if count > 0 { ink.text } else { ink.dim })
.child(SharedString::from(count.to_string())),
)
};
div()
.flex()
.flex_none()
.items_center()
.gap(px(2.0))
.h(px(BAR_HEIGHT))
.w_full()
.px(px(6.0))
.bg(ink.chrome)
.border_b_1()
.border_color(ink.border)
.child(
tool_button(
"devtools-dock-right",
IconName::PanelRight,
"Dock to right",
self.dock == Dock::Right,
ink,
cx,
)
.on_click(cx.listener(|this, _event, _window, cx| {
this.dock = Dock::Right;
cx.emit(DevToolsEvent::Dock(Dock::Right));
cx.notify();
})),
)
.child(
tool_button(
"devtools-dock-bottom",
IconName::PanelBottom,
"Dock to bottom",
self.dock == Dock::Bottom,
ink,
cx,
)
.on_click(cx.listener(|this, _event, _window, cx| {
this.dock = Dock::Bottom;
cx.emit(DevToolsEvent::Dock(Dock::Bottom));
cx.notify();
})),
)
.child(div().w(px(6.0)))
.child(
tool_button(
"devtools-pick",
IconName::Crosshair,
"Select an element",
self.picking,
ink,
cx,
)
.on_click(cx.listener(|this, _event, _window, cx| {
let picking = !this.picking;
this.set_picking(picking, cx);
})),
)
.child(
tool_button(
"devtools-drawer",
IconName::ScrollText,
"Show logs drawer",
self.drawer_open,
ink,
cx,
)
.on_click(cx.listener(|this, _event, _window, cx| this.toggle_drawer(cx))),
)
.child(
div()
.flex()
.flex_1()
.items_center()
.justify_center()
.gap(px(10.0))
.mx(px(8.0))
.h(px(19.0))
.rounded(px(4.0))
.bg(ink.content)
.border_1()
.border_color(ink.border)
.text_size(px(LABEL_SIZE))
.text_color(ink.dim)
.child(
div()
.flex()
.items_center()
.gap(px(3.0))
.child(glyph(IconName::Boxes, 11.0, ink.dim, cx))
.child(SharedString::from(format!("{} elements", self.tree.len()))),
)
.child(
div()
.flex()
.items_center()
.gap(px(3.0))
.child(glyph(IconName::ArrowUpDown, 11.0, ink.dim, cx))
.child(SharedString::from(format!(
"{requests} requests · {}",
format_bytes(transfer)
))),
)
.child(badge(warnings, IconName::TriangleAlert, ink.warning, cx))
.child(badge(errors, IconName::CircleX, ink.danger, cx)),
)
.child(
tool_button(
"devtools-clear",
IconName::Ban,
"Clear all records",
false,
ink,
cx,
)
.on_click(cx.listener(|_this, _event, _window, cx| {
if cx.has_global::<DevToolsState>() {
cx.update_global::<DevToolsState, _>(|state, _cx| state.clear_all());
}
cx.notify();
})),
)
.child(
tool_button("devtools-close", IconName::X, "Close", false, ink, cx).on_click(
cx.listener(|_this, _event, _window, cx| cx.emit(DevToolsEvent::Close)),
),
)
}
fn tab_bar(&self, ink: &Ink, cx: &mut Context<Self>) -> gpui::Div {
let mut bar = div()
.flex()
.flex_none()
.items_center()
.gap(px(1.0))
.h(px(BAR_HEIGHT))
.w_full()
.px(px(4.0))
.bg(ink.chrome)
.border_b_1()
.border_color(ink.border);
for tab in DevToolsTab::ALL {
let active = self.tab == tab;
let fg = if active { ink.text } else { ink.dim };
let hover_bg = ink.hover;
bar = bar.child(
div()
.id(("devtools-tab", tab as usize))
.flex()
.flex_none()
.items_center()
.gap(px(4.0))
.h(px(22.0))
.px(px(8.0))
.rounded(px(4.0))
.text_size(px(LABEL_SIZE))
.text_color(fg)
.when(active, |el| el.bg(ink.chrome_active))
.when(!active, |el| el.hover(move |st| st.bg(hover_bg)))
.child(glyph(tab.icon(), 12.0, fg, cx))
.child(SharedString::new_static(tab.label()))
.on_click(cx.listener(move |this, _event, _window, cx| {
this.tab = tab;
cx.notify();
})),
);
}
bar
}
fn status_bar(&self, ink: &Ink, cx: &mut Context<Self>) -> gpui::Div {
let fps = self
.timelines
.recording
.then(|| {
cx.try_global::<DevToolsState>()
.and_then(|state| state.fps())
})
.flatten();
div()
.flex()
.flex_none()
.items_center()
.gap(px(10.0))
.h(px(20.0))
.w_full()
.px(px(8.0))
.bg(ink.chrome)
.border_t_1()
.border_color(ink.border)
.text_size(px(LABEL_SIZE))
.text_color(ink.dim)
.child(SharedString::new_static("guise devtools"))
.child(div().flex_1())
.when_some(fps, |el, fps| {
el.child(SharedString::from(format!("{fps:.0} fps")))
})
.child(SharedString::from(self.tab.label()))
}
}
impl Render for DevTools {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
probe::begin_frame();
self.tree = probe::tree();
if self.elements.selected.is_none() {
if let Some(root) = self.tree.roots.first() {
let key = self.tree.nodes[*root].key.clone();
self.elements.select(key);
}
}
if self.timelines.recording && cx.has_global::<DevToolsState>() {
cx.update_global::<DevToolsState, _>(|state, _cx| state.record_frame());
}
let ink = Ink::read(cx);
let body = match self.tab {
DevToolsTab::Elements => self.elements.render(&self.tree, window, cx),
DevToolsTab::Network => self.network.render(window, cx),
DevToolsTab::Sources => self.sources.render(&self.tree, window, cx),
DevToolsTab::Timelines => self.timelines.render(window, cx),
DevToolsTab::Storage => self.storage.render(window, cx),
DevToolsTab::Layers => {
elements::layers_view(&self.tree, self.elements.selected.as_ref(), &ink, cx)
}
DevToolsTab::Logs => self.logs.render(window, cx),
DevToolsTab::Audit => self.audit.render(&self.tree, window, cx),
};
let drawer = (self.drawer_open && self.tab != DevToolsTab::Logs).then(|| {
div()
.flex()
.flex_col()
.flex_none()
.h(px(200.0))
.w_full()
.child(hairline(&ink))
.child(self.logs.render(window, cx))
});
div()
.track_focus(&self.focus)
.key_context("DevTools")
.flex()
.flex_col()
.size_full()
.min_h(px(0.0))
.bg(ink.content)
.text_color(ink.text)
.child(self.toolbar(&ink, cx))
.child(self.tab_bar(&ink, cx))
.child(
div()
.flex()
.flex_col()
.flex_1()
.min_h(px(0.0))
.w_full()
.child(body),
)
.children(drawer)
.child(self.status_bar(&ink, cx))
}
}
impl Drop for DevTools {
fn drop(&mut self) {
probe::release();
}
}
#[track_caller]
pub fn log(cx: &mut App, level: LogLevel, message: impl Into<SharedString>) {
let source = SourceRef::from(std::panic::Location::caller());
log_record(cx, LogRecord::new(level, message).source(source));
}
pub fn log_record(cx: &mut App, record: LogRecord) {
if cx.has_global::<DevToolsState>() {
cx.update_global::<DevToolsState, _>(|state, _cx| state.push_log(record));
}
}
pub fn network_begin(cx: &mut App, record: NetworkRecord) -> Option<u64> {
if !cx.has_global::<DevToolsState>() {
return None;
}
Some(cx.update_global::<DevToolsState, _>(|state, _cx| state.push_network(record)))
}
pub fn network_update(cx: &mut App, id: u64, f: impl FnOnce(&mut NetworkRecord)) {
if cx.has_global::<DevToolsState>() {
cx.update_global::<DevToolsState, _>(|state, _cx| state.update_network(id, f));
}
}
pub fn storage_set(cx: &mut App, domain: StorageDomain) {
if cx.has_global::<DevToolsState>() {
cx.update_global::<DevToolsState, _>(|state, _cx| state.set_storage(domain));
}
}
pub fn storage_remove(cx: &mut App, id: &str) {
if cx.has_global::<DevToolsState>() {
cx.update_global::<DevToolsState, _>(|state, _cx| state.remove_storage(id));
}
}
pub fn timeline_event(cx: &mut App, event: TimelineEvent) {
if cx.has_global::<DevToolsState>() {
cx.update_global::<DevToolsState, _>(|state, _cx| state.push_timeline(event));
}
}
pub fn measure<R>(cx: &mut App, label: impl Into<SharedString>, f: impl FnOnce() -> R) -> R {
let start = std::time::Instant::now();
let result = f();
timeline_event(
cx,
TimelineEvent::new(TimelineKind::Script, label, start.elapsed()),
);
result
}
pub fn clear(cx: &mut App) {
if cx.has_global::<DevToolsState>() {
cx.update_global::<DevToolsState, _>(|state, _cx| state.clear_all());
}
}
pub fn is_recording() -> bool {
probe::is_enabled()
}