Source code
Revision control
Copy as Markdown
Other Tools
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
mod debug_flags;
mod profiler;
mod shell;
mod textures;
mod composite_view;
use eframe::egui;
use webrender_api::DebugFlags;
use webrender_api::debugger::{DebuggerMessage, DebuggerTextureContent, ProfileCounterId, CompositorDebugInfo};
use crate::{command, net};
use std::collections::{HashMap, BTreeMap};
use std::fs;
use std::io::Write;
use std::sync::mpsc;
use profiler::Graph;
#[allow(dead_code)]
enum ApplicationEvent {
RunCommand(String),
NetworkEvent(net::NetworkEvent),
}
struct DataModel {
is_connected: bool,
debug_flags: DebugFlags,
cmd: String,
log: Vec<String>,
documents: Vec<Document>,
preview_doc_index: Option<usize>,
profile_graphs: HashMap<ProfileCounterId, Graph>,
}
impl DataModel {
fn new() -> Self {
DataModel {
is_connected: false,
debug_flags: DebugFlags::empty(),
cmd: String::new(),
log: Vec::new(),
documents: Vec::new(),
preview_doc_index: None,
profile_graphs: HashMap::new(),
}
}
}
#[derive(serde::Serialize, serde::Deserialize)]
pub enum Tool {
DebugFlags,
Profiler,
Shell,
Documents,
Preview,
}
impl egui_tiles::Behavior<Tool> for Gui {
fn tab_title_for_pane(&mut self, tool: &Tool) -> egui::WidgetText {
let title = match tool {
Tool::DebugFlags => { "Debug flags" }
Tool::Profiler => { "Profiler" }
Tool::Shell => { "Shell" }
Tool::Documents => { "Documents" }
Tool::Preview => { "Preview" }
};
title.into()
}
fn pane_ui(&mut self, ui: &mut egui::Ui, _id: egui_tiles::TileId, tool: &mut Tool) -> egui_tiles::UiResponse {
// Add a bit of margin around the panes, otherwise their content hugs the
// border in an awkward way. There may be a setting somewhere in egui_tiles
// rather than doing it manually.
egui::Frame::new().inner_margin(egui::Margin::symmetric(5, 5)).show(ui, |ui| {
match tool {
Tool::Documents => { do_documents_ui(self, ui); }
Tool::Preview => { do_preview_ui(self, ui); }
Tool::DebugFlags => { debug_flags::ui(self, ui); }
Tool::Profiler => { profiler::ui(self, ui); }
Tool::Shell => { shell::ui(self, ui); }
}
});
Default::default()
}
fn simplification_options(&self) -> egui_tiles::SimplificationOptions {
let mut options = egui_tiles::SimplificationOptions::default();
options.all_panes_must_have_tabs = true;
options
}
fn tab_title_spacing(&self, _visuals: &egui::Visuals) -> f32 {
50.0
}
fn gap_width(&self, _style: &egui::Style) -> f32 {
2.0
}
fn tab_outline_stroke(
&self,
_visuals: &egui::Visuals,
_tiles: &egui_tiles::Tiles<Tool>,
_tile_id: egui_tiles::TileId,
_state: &egui_tiles::TabState,
) -> egui::Stroke {
egui::Stroke::NONE
}
}
pub struct Gui {
data_model: DataModel,
net: net::HttpConnection,
cmd_list: command::CommandList,
cmd_history: Vec<String>,
cmd_history_index: usize,
doc_id: usize,
event_receiver: mpsc::Receiver<ApplicationEvent>,
ui_tiles: Option<egui_tiles::Tree<Tool>>,
}
impl Gui {
pub fn new(host: &str, cmd_list: command::CommandList) -> Self {
let net = net::HttpConnection::new(host);
let data_model = DataModel::new();
let (event_sender, event_receiver) = mpsc::channel();
// Spawn network event thread
let host_clone = host.to_string();
std::thread::spawn(move || {
net::NetworkEventStream::spawn(&host_clone, move |event| {
let _ = event_sender.send(ApplicationEvent::NetworkEvent(event));
});
});
// Try to load saved ui_tiles state, or create default layout
let save = fs::read_to_string(config_path())
.ok()
.and_then(|content| {
serde_json::from_str::<GuiSavedState>(&content)
.map_err(|e| {
eprintln!("Failed to deserialize ui_tiles state: {}", e);
e
})
.ok()
}).unwrap_or_else(|| {
// Create default layout
let mut tiles = egui_tiles::Tiles::default();
let tabs = vec![
tiles.insert_pane(Tool::Profiler),
tiles.insert_pane(Tool::Preview),
];
let side = vec![
tiles.insert_pane(Tool::DebugFlags),
tiles.insert_pane(Tool::Documents),
];
let side = tiles.insert_vertical_tile(side);
let main_tile = tiles.insert_tab_tile(tabs);
let main_and_side = vec![
side,
main_tile,
];
let main_and_side = tiles.insert_horizontal_tile(main_and_side);
let v = vec![
main_and_side,
tiles.insert_pane(Tool::Shell),
];
let root = tiles.insert_vertical_tile(v);
GuiSavedState {
cmd_history: Vec::new(),
ui_tiles: egui_tiles::Tree::new("WR debugger", root, tiles),
}
});
Gui {
data_model,
net,
cmd_list,
cmd_history: save.cmd_history,
cmd_history_index: 0,
doc_id: 0,
event_receiver,
ui_tiles: Some(save.ui_tiles),
}
}
pub fn run(self) {
let native_options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([1280.0, 720.0])
.with_title("WebRender Debug UI"),
..Default::default()
};
let _ = eframe::run_native(
"WebRender Debug UI",
native_options,
Box::new(|cc| {
// Load fonts
let mut fonts = egui::FontDefinitions::default();
fonts.font_data.insert(
"FiraSans".to_owned(),
egui::FontData::from_static(include_bytes!("../../res/FiraSans-Regular.ttf")).into(),
);
fonts.font_data.insert(
"FiraCode".to_owned(),
egui::FontData::from_static(include_bytes!("../../res/FiraCode-Regular.ttf")).into(),
);
fonts.families.get_mut(&egui::FontFamily::Proportional).unwrap()
.insert(0, "FiraSans".to_owned());
fonts.families.get_mut(&egui::FontFamily::Monospace).unwrap()
.insert(0, "FiraCode".to_owned());
cc.egui_ctx.set_fonts(fonts);
// Load layout settings if available
if let Ok(_ini) = fs::read_to_string("default-layout.ini") {
// egui uses a different state persistence mechanism
// This would need to be adapted for egui's state system
}
Ok(Box::new(self))
}),
);
}
}
impl eframe::App for Gui {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
// Process pending events
while let Ok(event) = self.event_receiver.try_recv() {
match event {
ApplicationEvent::RunCommand(cmd_name) => {
self.handle_command(&cmd_name);
}
ApplicationEvent::NetworkEvent(net_event) => {
self.handle_network_event(net_event);
}
}
}
textures::prepare(self, ctx);
// Main menu bar
egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
egui::MenuBar::new().ui(ui, |ui| {
ui.menu_button("File", |ui| {
if ui.button("Exit").clicked() {
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
}
});
ui.menu_button("Help", |ui| {
ui.label("About");
});
// Connection status on the right
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let (msg, text_color, bg_color) = if self.data_model.is_connected {
("Connected", egui::Color32::from_rgb(48, 48, 48), egui::Color32::from_rgb(0, 255, 0))
} else {
("Disconnected", egui::Color32::WHITE, egui::Color32::from_rgb(255, 0, 0))
};
// TODO: measure the text instead.
let mut area = ui.max_rect();
area.min.x = area.max.x - 85.0;
area.max.x += 10.0;
ui.painter().rect_filled(area, 0, bg_color);
let label = egui::Label::new(
egui::RichText::new(msg).color(text_color)
);
ui.add(label).on_hover_text("Connection status");
});
});
});
let mut ui_tiles = self.ui_tiles.take().unwrap();
egui::CentralPanel::default().show(ctx, |ui| {
ui.spacing_mut().window_margin = egui::Margin::ZERO;
ui_tiles.ui(self, ui);
});
self.ui_tiles = Some(ui_tiles);
}
}
impl Gui {
fn handle_command(&mut self, cmd_name: &str) {
match self.cmd_list.get_mut(cmd_name) {
Some(cmd) => {
let mut ctx = command::CommandContext::new(
BTreeMap::new(),
&mut self.net,
);
let output = cmd.run(&mut ctx);
match output {
command::CommandOutput::Log(msg) => {
self.data_model.log.push(msg);
}
command::CommandOutput::Err(msg) => {
self.data_model.log.push(msg);
}
command::CommandOutput::TextDocument { title, content } => {
let title = format!("{} [id {}]", title, self.doc_id);
self.doc_id += 1;
self.data_model.preview_doc_index = Some(self.data_model.documents.len());
self.data_model.documents.push(
Document {
title,
kind: DocumentKind::Text {
content,
}
}
);
}
command::CommandOutput::SerdeDocument { kind, ref content } => {
let title = format!("Compositor [id {}]", self.doc_id);
self.doc_id += 1;
self.data_model.preview_doc_index = Some(self.data_model.documents.len());
let kind = match kind.as_str() {
"composite-view" => {
let info = serde_json::from_str(content).unwrap();
DocumentKind::Compositor {
info,
}
}
_ => {
unreachable!("unknown content");
}
};
self.data_model.documents.push(
Document {
title,
kind,
}
);
}
command::CommandOutput::Textures(textures) => {
textures::add_textures(self, textures);
}
}
}
None => {
self.data_model.log.push(
format!("Unknown command '{}'", cmd_name)
);
}
}
}
fn handle_network_event(&mut self, event: net::NetworkEvent) {
match event {
net::NetworkEvent::Connected => {
self.data_model.is_connected = true;
}
net::NetworkEvent::Disconnected => {
self.data_model.is_connected = false;
}
net::NetworkEvent::Message(msg) => {
match msg {
DebuggerMessage::SetDebugFlags(info) => {
self.data_model.debug_flags = info.flags;
}
DebuggerMessage::InitProfileCounters(info) => {
let selected_counters = [
"Frame building",
"Renderer",
];
for counter in info.counters {
if selected_counters.contains(&counter.name.as_str()) {
println!("Add profile counter {:?}", counter.name);
self.data_model.profile_graphs.insert(
counter.id,
Graph::new(&counter.name, 512),
);
}
}
}
DebuggerMessage::UpdateProfileCounters(info) => {
for counter in &info.updates {
if let Some(graph) = self.data_model
.profile_graphs
.get_mut(&counter.id) {
graph.push(counter.value);
}
}
}
}
}
}
}
}
impl Drop for Gui {
fn drop(&mut self) {
// Serialize and save UI state
const MAX_HISTORY_SAVED: usize = 50;
let hist_len = self.cmd_history.len();
let range = if hist_len > MAX_HISTORY_SAVED {
hist_len - MAX_HISTORY_SAVED..hist_len
} else {
0..hist_len
};
let save = GuiSavedState {
ui_tiles: self.ui_tiles.take().unwrap(),
cmd_history: self.cmd_history.drain(range).collect(),
};
match serde_json::to_string(&save) {
Ok(serialized) => {
if let Err(e) = fs::File::create(config_path())
.and_then(|mut file| file.write_all(serialized.as_bytes()))
{
eprintln!("Failed to save ui_tiles state: {}", e);
}
}
Err(e) => {
eprintln!("Failed to serialize ui_tiles: {}", e);
}
}
}
}
pub enum DocumentKind {
Text {
content: String,
},
Compositor {
info: CompositorDebugInfo,
},
Texture {
content: DebuggerTextureContent,
handle: Option<egui::TextureHandle>,
}
}
pub struct Document {
pub title: String,
pub kind: DocumentKind,
}
fn do_documents_ui(app: &mut Gui, ui: &mut egui::Ui) {
let width = ui.available_width();
for (i, doc) in app.data_model.documents.iter().enumerate() {
if let DocumentKind::Texture { .. } = doc.kind {
// Handle textures separately below.
continue;
}
let item = egui::Button::selectable(
app.data_model.preview_doc_index == Some(i),
&doc.title,
).min_size(egui::vec2(width, 20.0));
if ui.add(item).clicked() {
app.data_model.preview_doc_index = Some(i);
}
}
textures::texture_list_ui(app, ui);
}
fn do_preview_ui(app: &mut Gui, ui: &mut egui::Ui) {
if let Some(idx) = app.data_model.preview_doc_index {
if idx >= app.data_model.documents.len() {
app.data_model.preview_doc_index = None;
return;
}
let doc = &app.data_model.documents[idx];
match &doc.kind {
DocumentKind::Text { content } => {
egui::ScrollArea::both().show(ui, |ui| {
ui.label(egui::RichText::new(content).monospace());
});
}
DocumentKind::Compositor { .. } => {
// We need to handle compositor separately due to borrow checker
if let Some(Document { kind: DocumentKind::Compositor { info }, .. }) =
app.data_model.documents.get_mut(idx) {
composite_view::ui(ui, info);
}
}
DocumentKind::Texture { content, handle } => {
if let Some(handle) = handle {
textures::texture_viewer_ui(ui, &content, &handle);
}
}
}
}
}
// TODO: It would be better to use confy::load/store instead of loading
// files manually but for some reason serialization of the ui tiles panics
// in confy.
fn config_path() -> std::path::PathBuf {
confy::get_configuration_file_path("wr_debugger", None).unwrap()
}
#[derive(serde::Serialize, serde::Deserialize)]
struct GuiSavedState {
cmd_history: Vec<String>,
ui_tiles: egui_tiles::Tree<Tool>,
}