mod events;
mod helpers;
mod inspector;
mod profiler;
mod state;
mod style;
mod time_travel;
pub use events::{EventFilter, EventLogger, EventType, LoggedEvent};
pub use inspector::{ComponentPicker, Inspector, InspectorConfig, PickerMode, WidgetNode};
pub use profiler::{ComponentStats, Frame, Profiler, ProfilerView, RenderEvent, RenderReason};
pub use state::{StateDebugger, StateEntry, StateValue};
pub use style::{ComputedProperty, PropertySource, StyleCategory, StyleInspector};
pub use time_travel::{
Action, SnapshotValue, StateDiff, StateSnapshot, TimeTravelConfig, TimeTravelDebugger,
TimeTravelView,
};
use crate::layout::Rect;
use crate::render::Buffer;
use crate::style::Color;
use std::sync::atomic::{AtomicBool, Ordering};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DevToolsPosition {
#[default]
Right,
Bottom,
Left,
Overlay,
}
#[derive(Debug, Clone)]
pub struct DevToolsConfig {
pub position: DevToolsPosition,
pub size: u16,
pub visible: bool,
pub active_tab: DevToolsTab,
pub bg_color: Color,
pub fg_color: Color,
pub accent_color: Color,
}
impl Default for DevToolsConfig {
fn default() -> Self {
Self {
position: DevToolsPosition::Right,
size: 50,
visible: false,
active_tab: DevToolsTab::Inspector,
bg_color: Color::rgb(25, 25, 35),
fg_color: Color::rgb(200, 200, 210),
accent_color: Color::rgb(130, 180, 255),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DevToolsTab {
#[default]
Inspector,
State,
Styles,
Events,
Profiler,
TimeTravel,
}
impl DevToolsTab {
pub fn label(&self) -> &'static str {
match self {
Self::Inspector => "Inspector",
Self::State => "State",
Self::Styles => "Styles",
Self::Events => "Events",
Self::Profiler => "Profiler",
Self::TimeTravel => "Travel",
}
}
pub fn all() -> &'static [DevToolsTab] {
&[
DevToolsTab::Inspector,
DevToolsTab::State,
DevToolsTab::Styles,
DevToolsTab::Events,
DevToolsTab::Profiler,
DevToolsTab::TimeTravel,
]
}
pub fn next(&self) -> Self {
match self {
Self::Inspector => Self::State,
Self::State => Self::Styles,
Self::Styles => Self::Events,
Self::Events => Self::Profiler,
Self::Profiler => Self::TimeTravel,
Self::TimeTravel => Self::Inspector,
}
}
pub fn prev(&self) -> Self {
match self {
Self::Inspector => Self::TimeTravel,
Self::State => Self::Inspector,
Self::Styles => Self::State,
Self::Events => Self::Styles,
Self::Profiler => Self::Events,
Self::TimeTravel => Self::Profiler,
}
}
}
pub struct DevTools {
config: DevToolsConfig,
inspector: Inspector,
state: StateDebugger,
styles: StyleInspector,
events: EventLogger,
profiler: Profiler,
time_travel: TimeTravelDebugger,
}
impl DevTools {
pub fn new() -> Self {
Self {
config: DevToolsConfig::default(),
inspector: Inspector::new(),
state: StateDebugger::new(),
styles: StyleInspector::new(),
events: EventLogger::new(),
profiler: Profiler::new(),
time_travel: TimeTravelDebugger::new(),
}
}
pub fn config(mut self, config: DevToolsConfig) -> Self {
self.config = config;
self
}
pub fn position(mut self, position: DevToolsPosition) -> Self {
self.config.position = position;
self
}
pub fn size(mut self, size: u16) -> Self {
self.config.size = size;
self
}
pub fn toggle(&mut self) {
self.config.visible = !self.config.visible;
}
pub fn set_visible(&mut self, visible: bool) {
self.config.visible = visible;
}
pub fn is_visible(&self) -> bool {
self.config.visible
}
pub fn set_tab(&mut self, tab: DevToolsTab) {
self.config.active_tab = tab;
}
pub fn next_tab(&mut self) {
self.config.active_tab = self.config.active_tab.next();
}
pub fn prev_tab(&mut self) {
self.config.active_tab = self.config.active_tab.prev();
}
pub fn inspector(&self) -> &Inspector {
&self.inspector
}
pub fn inspector_mut(&mut self) -> &mut Inspector {
&mut self.inspector
}
pub fn state(&self) -> &StateDebugger {
&self.state
}
pub fn state_mut(&mut self) -> &mut StateDebugger {
&mut self.state
}
pub fn styles(&self) -> &StyleInspector {
&self.styles
}
pub fn styles_mut(&mut self) -> &mut StyleInspector {
&mut self.styles
}
pub fn events(&self) -> &EventLogger {
&self.events
}
pub fn events_mut(&mut self) -> &mut EventLogger {
&mut self.events
}
pub fn profiler(&self) -> &Profiler {
&self.profiler
}
pub fn profiler_mut(&mut self) -> &mut Profiler {
&mut self.profiler
}
pub fn time_travel(&self) -> &TimeTravelDebugger {
&self.time_travel
}
pub fn time_travel_mut(&mut self) -> &mut TimeTravelDebugger {
&mut self.time_travel
}
pub fn panel_rect(&self, area: Rect) -> Option<Rect> {
if !self.config.visible {
return None;
}
let size = self.config.size;
Some(match self.config.position {
DevToolsPosition::Right => Rect::new(
area.x + area.width.saturating_sub(size),
area.y,
size.min(area.width),
area.height,
),
DevToolsPosition::Left => Rect::new(area.x, area.y, size.min(area.width), area.height),
DevToolsPosition::Bottom => Rect::new(
area.x,
area.y + area.height.saturating_sub(size),
area.width,
size.min(area.height),
),
DevToolsPosition::Overlay => {
let width = (area.width * 2 / 3).min(80);
let height = (area.height * 2 / 3).min(30);
Rect::new(
area.x + (area.width - width) / 2,
area.y + (area.height - height) / 2,
width,
height,
)
}
})
}
pub fn content_rect(&self, area: Rect) -> Rect {
if !self.config.visible {
return area;
}
let size = self.config.size;
match self.config.position {
DevToolsPosition::Right => {
Rect::new(area.x, area.y, area.width.saturating_sub(size), area.height)
}
DevToolsPosition::Left => Rect::new(
area.x + size.min(area.width),
area.y,
area.width.saturating_sub(size),
area.height,
),
DevToolsPosition::Bottom => {
Rect::new(area.x, area.y, area.width, area.height.saturating_sub(size))
}
DevToolsPosition::Overlay => area,
}
}
pub fn render(&self, buffer: &mut Buffer, area: Rect) {
if let Some(panel) = self.panel_rect(area) {
self.render_panel(buffer, panel);
}
}
fn render_panel(&self, buffer: &mut Buffer, area: Rect) {
for y in area.y..area.y + area.height {
for x in area.x..area.x + area.width {
if let Some(cell) = buffer.get_mut(x, y) {
cell.symbol = ' ';
cell.bg = Some(self.config.bg_color);
cell.fg = Some(self.config.fg_color);
}
}
}
self.draw_border(buffer, area);
let tab_area = Rect::new(area.x + 1, area.y + 1, area.width - 2, 1);
self.render_tabs(buffer, tab_area);
let content_area = Rect::new(
area.x + 1,
area.y + 3,
area.width - 2,
area.height.saturating_sub(4),
);
match self.config.active_tab {
DevToolsTab::Inspector => {
self.inspector
.render_content(buffer, content_area, &self.config)
}
DevToolsTab::State => self
.state
.render_content(buffer, content_area, &self.config),
DevToolsTab::Styles => self
.styles
.render_content(buffer, content_area, &self.config),
DevToolsTab::Events => self
.events
.render_content(buffer, content_area, &self.config),
DevToolsTab::Profiler => {
self.profiler
.render_content(buffer, content_area, &self.config)
}
DevToolsTab::TimeTravel => {
self.time_travel
.render_content(buffer, content_area, &self.config)
}
}
}
fn render_tabs(&self, buffer: &mut Buffer, area: Rect) {
let mut x = area.x;
for tab in DevToolsTab::all() {
let label = format!(" {} ", tab.label());
let is_active = *tab == self.config.active_tab;
let (fg, bg) = if is_active {
(self.config.bg_color, self.config.accent_color)
} else {
(self.config.fg_color, self.config.bg_color)
};
for ch in label.chars() {
if x < area.x + area.width {
if let Some(cell) = buffer.get_mut(x, area.y) {
cell.symbol = ch;
cell.fg = Some(fg);
cell.bg = Some(bg);
}
x += 1;
}
}
x += 1; }
}
fn draw_border(&self, buffer: &mut Buffer, area: Rect) {
let color = self.config.accent_color;
for x in area.x..area.x + area.width {
if let Some(cell) = buffer.get_mut(x, area.y) {
cell.symbol = if x == area.x {
'┌'
} else if x == area.x + area.width - 1 {
'┐'
} else {
'─'
};
cell.fg = Some(color);
}
if let Some(cell) = buffer.get_mut(x, area.y + area.height - 1) {
cell.symbol = if x == area.x {
'└'
} else if x == area.x + area.width - 1 {
'┘'
} else {
'─'
};
cell.fg = Some(color);
}
}
for y in area.y + 1..area.y + area.height - 1 {
if let Some(cell) = buffer.get_mut(area.x, y) {
cell.symbol = '│';
cell.fg = Some(color);
}
if let Some(cell) = buffer.get_mut(area.x + area.width - 1, y) {
cell.symbol = '│';
cell.fg = Some(color);
}
}
for x in area.x..area.x + area.width {
if let Some(cell) = buffer.get_mut(x, area.y + 2) {
cell.symbol = if x == area.x {
'├'
} else if x == area.x + area.width - 1 {
'┤'
} else {
'─'
};
cell.fg = Some(color);
}
}
}
}
impl Default for DevTools {
fn default() -> Self {
Self::new()
}
}
static DEVTOOLS_ENABLED: AtomicBool = AtomicBool::new(false);
#[deprecated(since = "2.1.0", note = "Use App::enable_devtools() instead")]
pub fn enable_devtools() {
DEVTOOLS_ENABLED.store(true, Ordering::Relaxed);
}
#[deprecated(since = "2.1.0", note = "Use App::disable_devtools() instead")]
pub fn disable_devtools() {
DEVTOOLS_ENABLED.store(false, Ordering::Relaxed);
}
#[deprecated(since = "2.1.0", note = "Use App::is_devtools_enabled() instead")]
pub fn is_devtools_enabled() -> bool {
DEVTOOLS_ENABLED.load(Ordering::Relaxed)
}
#[deprecated(since = "2.1.0", note = "Use App::toggle_devtools() instead")]
pub fn toggle_devtools() -> bool {
let was = DEVTOOLS_ENABLED.fetch_xor(true, Ordering::Relaxed);
!was
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::app::App;
#[test]
fn test_devtools_config_default() {
let config = DevToolsConfig::default();
assert!(!config.visible);
assert_eq!(config.position, DevToolsPosition::Right);
assert_eq!(config.active_tab, DevToolsTab::Inspector);
assert_eq!(config.size, 50);
}
#[test]
fn test_devtools_tab_cycle() {
let tab = DevToolsTab::Inspector;
assert_eq!(tab.next(), DevToolsTab::State);
assert_eq!(tab.prev(), DevToolsTab::TimeTravel);
}
#[test]
fn test_devtools_toggle() {
let mut devtools = DevTools::new();
assert!(!devtools.is_visible());
devtools.toggle();
assert!(devtools.is_visible());
devtools.toggle();
assert!(!devtools.is_visible());
}
#[test]
fn test_panel_rect_right() {
let devtools = DevTools::new().size(30);
let mut dt = devtools;
dt.set_visible(true);
let area = Rect::new(0, 0, 100, 50);
let panel = dt.panel_rect(area).unwrap();
assert_eq!(panel.x, 70);
assert_eq!(panel.width, 30);
assert_eq!(panel.height, 50);
}
#[test]
fn test_panel_rect_left() {
let mut devtools = DevTools::new().size(25);
devtools.set_visible(true);
devtools.config.position = DevToolsPosition::Left;
let area = Rect::new(10, 5, 100, 50);
let panel = devtools.panel_rect(area).unwrap();
assert_eq!(panel.x, 10);
assert_eq!(panel.y, 5);
assert_eq!(panel.width, 25);
assert_eq!(panel.height, 50);
}
#[test]
fn test_panel_rect_bottom() {
let mut devtools = DevTools::new().size(15);
devtools.set_visible(true);
devtools.config.position = DevToolsPosition::Bottom;
let area = Rect::new(0, 0, 100, 50);
let panel = devtools.panel_rect(area).unwrap();
assert_eq!(panel.x, 0);
assert_eq!(panel.y, 35);
assert_eq!(panel.width, 100);
assert_eq!(panel.height, 15);
}
#[test]
fn test_panel_rect_overlay() {
let mut devtools = DevTools::new();
devtools.set_visible(true);
devtools.config.position = DevToolsPosition::Overlay;
let area = Rect::new(0, 0, 100, 50);
let panel = devtools.panel_rect(area).unwrap();
assert_eq!(panel.width, 66); assert_eq!(panel.height, 30); }
#[test]
fn test_panel_rect_invisible() {
let mut devtools = DevTools::new();
devtools.set_visible(false);
let area = Rect::new(0, 0, 100, 50);
assert!(devtools.panel_rect(area).is_none());
}
#[test]
fn test_content_rect_right() {
let mut devtools = DevTools::new().size(30);
devtools.set_visible(true);
let area = Rect::new(0, 0, 100, 50);
let content = devtools.content_rect(area);
assert_eq!(content.x, 0);
assert_eq!(content.y, 0);
assert_eq!(content.width, 70);
assert_eq!(content.height, 50);
}
#[test]
fn test_content_rect_left() {
let mut devtools = DevTools::new().size(30);
devtools.set_visible(true);
devtools.config.position = DevToolsPosition::Left;
let area = Rect::new(0, 0, 100, 50);
let content = devtools.content_rect(area);
assert_eq!(content.x, 30);
assert_eq!(content.width, 70);
}
#[test]
fn test_content_rect_bottom() {
let mut devtools = DevTools::new().size(20);
devtools.set_visible(true);
devtools.config.position = DevToolsPosition::Bottom;
let area = Rect::new(0, 0, 100, 50);
let content = devtools.content_rect(area);
assert_eq!(content.y, 0);
assert_eq!(content.height, 30);
}
#[test]
fn test_content_rect_overlay() {
let mut devtools = DevTools::new();
devtools.set_visible(true);
devtools.config.position = DevToolsPosition::Overlay;
let area = Rect::new(0, 0, 100, 50);
let content = devtools.content_rect(area);
assert_eq!(content, area);
}
#[test]
fn test_content_rect_invisible() {
let mut devtools = DevTools::new();
devtools.set_visible(false);
let area = Rect::new(0, 0, 100, 50);
let content = devtools.content_rect(area);
assert_eq!(content, area);
}
#[test]
fn test_devtools_new() {
let devtools = DevTools::new();
assert!(!devtools.is_visible());
assert_eq!(devtools.config.active_tab, DevToolsTab::Inspector);
}
#[test]
fn test_devtools_default() {
let devtools = DevTools::default();
assert!(!devtools.is_visible());
assert_eq!(devtools.config.active_tab, DevToolsTab::Inspector);
}
#[test]
fn test_devtools_config_builder() {
let config = DevToolsConfig {
position: DevToolsPosition::Left,
size: 40,
visible: true,
active_tab: DevToolsTab::Profiler,
bg_color: Color::rgb(10, 10, 10),
fg_color: Color::rgb(255, 255, 255),
accent_color: Color::rgb(100, 100, 255),
};
let devtools = DevTools::new().config(config);
assert!(devtools.is_visible());
assert_eq!(devtools.config.position, DevToolsPosition::Left);
assert_eq!(devtools.config.size, 40);
assert_eq!(devtools.config.active_tab, DevToolsTab::Profiler);
}
#[test]
fn test_devtools_position() {
let devtools = DevTools::new().position(DevToolsPosition::Bottom);
assert_eq!(devtools.config.position, DevToolsPosition::Bottom);
}
#[test]
fn test_devtools_size() {
let devtools = DevTools::new().size(60);
assert_eq!(devtools.config.size, 60);
}
#[test]
fn test_devtools_set_visible() {
let mut devtools = DevTools::new();
assert!(!devtools.is_visible());
devtools.set_visible(true);
assert!(devtools.is_visible());
devtools.set_visible(false);
assert!(!devtools.is_visible());
}
#[test]
fn test_devtools_set_tab() {
let mut devtools = DevTools::new();
assert_eq!(devtools.config.active_tab, DevToolsTab::Inspector);
devtools.set_tab(DevToolsTab::Profiler);
assert_eq!(devtools.config.active_tab, DevToolsTab::Profiler);
}
#[test]
fn test_devtools_next_tab() {
let mut devtools = DevTools::new();
assert_eq!(devtools.config.active_tab, DevToolsTab::Inspector);
devtools.next_tab();
assert_eq!(devtools.config.active_tab, DevToolsTab::State);
devtools.next_tab();
assert_eq!(devtools.config.active_tab, DevToolsTab::Styles);
}
#[test]
fn test_devtools_prev_tab() {
let mut devtools = DevTools::new();
assert_eq!(devtools.config.active_tab, DevToolsTab::Inspector);
devtools.prev_tab();
assert_eq!(devtools.config.active_tab, DevToolsTab::TimeTravel);
devtools.prev_tab();
assert_eq!(devtools.config.active_tab, DevToolsTab::Profiler);
}
#[test]
fn test_devtools_getters() {
let devtools = DevTools::new();
let _inspector = devtools.inspector();
let _state = devtools.state();
let _styles = devtools.styles();
let _events = devtools.events();
let _profiler = devtools.profiler();
let _time_travel = devtools.time_travel();
}
#[test]
fn test_devtools_getters_mut() {
let mut devtools = DevTools::new();
devtools.inspector_mut();
devtools.state_mut();
devtools.styles_mut();
devtools.events_mut();
devtools.profiler_mut();
devtools.time_travel_mut();
}
#[test]
fn test_devtools_tab_label() {
assert_eq!(DevToolsTab::Inspector.label(), "Inspector");
assert_eq!(DevToolsTab::State.label(), "State");
assert_eq!(DevToolsTab::Styles.label(), "Styles");
assert_eq!(DevToolsTab::Events.label(), "Events");
assert_eq!(DevToolsTab::Profiler.label(), "Profiler");
assert_eq!(DevToolsTab::TimeTravel.label(), "Travel");
}
#[test]
fn test_devtools_tab_all() {
let all = DevToolsTab::all();
assert_eq!(all.len(), 6);
assert_eq!(all[0], DevToolsTab::Inspector);
assert_eq!(all[5], DevToolsTab::TimeTravel);
}
#[test]
fn test_devtools_tab_next_cycle() {
assert_eq!(DevToolsTab::Inspector.next(), DevToolsTab::State);
assert_eq!(DevToolsTab::State.next(), DevToolsTab::Styles);
assert_eq!(DevToolsTab::Styles.next(), DevToolsTab::Events);
assert_eq!(DevToolsTab::Events.next(), DevToolsTab::Profiler);
assert_eq!(DevToolsTab::Profiler.next(), DevToolsTab::TimeTravel);
assert_eq!(DevToolsTab::TimeTravel.next(), DevToolsTab::Inspector);
}
#[test]
fn test_devtools_tab_prev_cycle() {
assert_eq!(DevToolsTab::Inspector.prev(), DevToolsTab::TimeTravel);
assert_eq!(DevToolsTab::TimeTravel.prev(), DevToolsTab::Profiler);
assert_eq!(DevToolsTab::Profiler.prev(), DevToolsTab::Events);
assert_eq!(DevToolsTab::Events.prev(), DevToolsTab::Styles);
assert_eq!(DevToolsTab::Styles.prev(), DevToolsTab::State);
assert_eq!(DevToolsTab::State.prev(), DevToolsTab::Inspector);
}
#[test]
fn test_devtools_position_default() {
assert_eq!(DevToolsPosition::default(), DevToolsPosition::Right);
}
#[test]
fn test_devtools_tab_default() {
assert_eq!(DevToolsTab::default(), DevToolsTab::Inspector);
}
#[test]
fn test_panel_rect_saturation() {
let mut devtools = DevTools::new().size(200);
devtools.set_visible(true);
let area = Rect::new(0, 0, 100, 50);
let panel = devtools.panel_rect(area).unwrap();
assert_eq!(panel.width, 100);
assert_eq!(panel.height, 50);
}
#[test]
fn test_content_rect_saturation() {
let mut devtools = DevTools::new().size(200);
devtools.set_visible(true);
let area = Rect::new(0, 0, 100, 50);
let content = devtools.content_rect(area);
assert_eq!(content.width, 0); assert_eq!(content.height, 50);
}
#[test]
fn test_global_enable_disable_devtools() {
let mut app = App::builder().devtools(false).build();
assert!(!app.is_devtools_enabled());
app.enable_devtools();
assert!(app.is_devtools_enabled());
app.disable_devtools();
assert!(!app.is_devtools_enabled());
}
#[test]
fn test_global_toggle_devtools() {
let mut app = App::builder().devtools(false).build();
assert!(!app.is_devtools_enabled());
let result = app.toggle_devtools();
assert!(result); assert!(app.is_devtools_enabled());
let result = app.toggle_devtools();
assert!(!result); assert!(!app.is_devtools_enabled());
}
}