tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 538ed294babe504cf5e08528c99b339519288831
parent 9793fb44a87cf3806de26314c18a525eb47e495c
Author: Nicolas Silva <nical@fastmail.com>
Date:   Tue,  9 Dec 2025 08:19:08 +0000

Bug 1998182 - Add a basic timeline widget to WR's debugger. r=gw

Differential Revision: https://phabricator.services.mozilla.com/D271377

Diffstat:
Mgfx/wr/wrshell/src/gui/draw_calls.rs | 3+--
Mgfx/wr/wrshell/src/gui/mod.rs | 57+++++++++++++++++++++++++++++++++++++++++++++++++--------
Agfx/wr/wrshell/src/gui/timeline.rs | 152+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 202 insertions(+), 10 deletions(-)

diff --git a/gfx/wr/wrshell/src/gui/draw_calls.rs b/gfx/wr/wrshell/src/gui/draw_calls.rs @@ -10,8 +10,7 @@ pub fn ui(app: &mut Gui, ui: &mut egui::Ui) { app.net.post_with_content("render-cmd-log", &app.data_model.frame_log.enabled).ok(); } - // TODO: select the frame using a timeline. - if let Some(frame) = app.data_model.frame_log.frames.back() { + if let Some(frame) = app.data_model.frame_log.frame(app.data_model.timeline.current_frame) { if let Some(cmds) = &frame.render_commands { draw_calls_ui(cmds, ui); } diff --git a/gfx/wr/wrshell/src/gui/mod.rs b/gfx/wr/wrshell/src/gui/mod.rs @@ -8,6 +8,7 @@ mod shell; mod textures; mod composite_view; mod draw_calls; +mod timeline; use eframe::egui; use webrender_api::{DebugFlags, RenderCommandInfo}; @@ -33,6 +34,7 @@ struct LoggedFrame { struct FrameLog { pub frames: VecDeque<LoggedFrame>, pub enabled: bool, + pub frames_end: usize, } impl FrameLog { @@ -40,8 +42,26 @@ impl FrameLog { FrameLog { frames: VecDeque::with_capacity(100), enabled: false, + frames_end: 0, } } + + pub fn first_frame_index(&self) -> usize { + self.frames_end - self.frames.len() + } + + pub fn last_frame_index(&self) -> usize { + self.frames_end.max(1) - 1 + } + + pub fn frame(&self, idx: usize) -> Option<&LoggedFrame> { + let i = idx - self.first_frame_index(); + if i < self.frames.len() { + return Some(&self.frames[i]); + } + + None + } } struct DataModel { @@ -53,6 +73,7 @@ struct DataModel { preview_doc_index: Option<usize>, profile_graphs: HashMap<ProfileCounterId, Graph>, frame_log: FrameLog, + timeline: timeline::Timeline, } impl DataModel { @@ -66,6 +87,7 @@ impl DataModel { preview_doc_index: None, profile_graphs: HashMap::new(), frame_log: FrameLog::new(), + timeline: timeline::Timeline::new(), } } } @@ -78,6 +100,7 @@ pub enum Tool { Documents, Preview, DrawCalls, + Timeline, } impl egui_tiles::Behavior<Tool> for Gui { @@ -89,6 +112,7 @@ impl egui_tiles::Behavior<Tool> for Gui { Tool::Documents => { "Documents" } Tool::Preview => { "Preview" } Tool::DrawCalls => { "Draw calls" } + Tool::Timeline => { "Timeline" } }; title.into() @@ -106,6 +130,7 @@ impl egui_tiles::Behavior<Tool> for Gui { Tool::Profiler => { profiler::ui(self, ui); } Tool::Shell => { shell::ui(self, ui); } Tool::DrawCalls => { draw_calls::ui(self, ui); } + Tool::Timeline => { timeline::ui(self, ui); } } }); @@ -197,12 +222,16 @@ impl Gui { side, main_tile, ]; - let main_and_side = tiles.insert_horizontal_tile(main_and_side); - let v = vec![ - main_and_side, + let shell_and_timeline = vec![ tiles.insert_pane(Tool::Shell), + tiles.insert_pane(Tool::Timeline), ]; - let root = tiles.insert_vertical_tile(v); + let shell_and_timeline = tiles.insert_tab_tile(shell_and_timeline); + let main_and_side = tiles.insert_horizontal_tile(main_and_side); + let root = tiles.insert_vertical_tile(vec![ + main_and_side, + shell_and_timeline, + ]); GuiSavedState { version: GuiSavedState::VERSION, @@ -432,12 +461,24 @@ impl Gui { } } } - if self.data_model.frame_log.frames.len() == self.data_model.frame_log.frames.capacity() { - self.data_model.frame_log.frames.pop_front(); + let frame_log = &mut self.data_model.frame_log; + if frame_log.frames.len() == frame_log.frames.capacity() { + frame_log.frames.pop_front(); } - self.data_model.frame_log.frames.push_back(LoggedFrame { + frame_log.frames.push_back(LoggedFrame { render_commands: info.render_commands.clone(), }); + frame_log.frames_end += 1; + let first = frame_log.first_frame_index(); + let last = frame_log.last_frame_index(); + + if self.data_model.timeline.current_frame < first { + self.data_model.timeline.current_frame = first; + } + + if self.data_model.timeline.current_frame >= last - 1 { + self.data_model.timeline.current_frame = last; + } } } } @@ -565,5 +606,5 @@ struct GuiSavedState { impl GuiSavedState { /// Update this number to reset the configuration. This ensures that new /// panels are added. - const VERSION: u32 = 1; + const VERSION: u32 = 2; } diff --git a/gfx/wr/wrshell/src/gui/timeline.rs b/gfx/wr/wrshell/src/gui/timeline.rs @@ -0,0 +1,152 @@ +/* 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use super::Gui; + +pub struct Timeline { + pub current_frame: usize, +} + +impl Timeline { + pub fn new() -> Self { + Timeline { + current_frame: 0, + } + } +} + +pub fn ui(app: &mut Gui, ui: &mut egui::Ui) { + let area = ui.max_rect(); + let mut top_area = area; + let mut bottom_area = area; + top_area.max.y -= 20.0; + bottom_area.min.y = top_area.max.y; + + ui.scope_builder(egui::UiBuilder::new().max_rect(bottom_area), |ui| { + show_timeline( + bottom_area, + &mut app.data_model.timeline.current_frame, + app.data_model.frame_log.first_frame_index(), + app.data_model.frame_log.last_frame_index(), + ui, + ); + }); +} + +pub fn show_timeline( + rect: egui::Rect, + current_frame: &mut usize, + first_frame: usize, + last_frame: usize, + ui: &mut egui::Ui, +) { + ui.horizontal(|ui| { + let style = ui.style().clone(); + let radius = style.visuals.widgets.inactive.corner_radius; + + let button_size = egui::Vec2 { x: 20.0, y: rect.height() }; + let prev = egui::Button::new("←") + .min_size(button_size) + .corner_radius(egui::CornerRadius { nw: radius.nw, sw: radius.sw, ne: 0, se: 0 }); + let next = egui::Button::new("→") + .min_size(button_size) + .corner_radius(egui::CornerRadius { ne: radius.ne, se: radius.se, nw: 0, sw: 0 }); + + let spacing = ui.spacing().item_spacing.x; + ui.spacing_mut().item_spacing.x = 1.0; + + let prev_clicked = ui.add(prev).clicked(); + + ui.spacing_mut().item_spacing.x = spacing; + + let next_clicked = ui.add(next).clicked(); + + if prev_clicked { + *current_frame = (*current_frame).max(first_frame + 1) - 1 + } + + if next_clicked { + *current_frame = (*current_frame + 1).min(last_frame) + } + + let min = ui.cursor().min; + let max = rect.max; + let tl_rect = egui::Rect { min, max }; + let size = max - min; + + let sense = egui::Sense::CLICK + | egui::Sense::HOVER + | egui::Sense::FOCUSABLE + | egui::Sense::DRAG; + let response = ui.allocate_response(size, sense); + + let num_frames = last_frame - first_frame + 1; + let n = num_frames as f32; + + let y0 = min.y; + let y1 = min.y + size.y; + + let background = style.visuals.widgets.inactive.bg_fill; + let border = style.visuals.widgets.inactive.bg_stroke; + let separator = egui::Stroke { width: 1.0, color: style.visuals.panel_fill }; + ui.painter().rect(tl_rect, radius, background, border, egui::StrokeKind::Inside); + + let mut hovered_frame = None; + let mut prev_x = min.x; + for i in 0..num_frames { + let x = min.x + (i + 1) as f32 * size.x / n; + + let frame_rect = egui::Rect { + min: egui::Pos2 { x: prev_x, y: y0 }, + max: egui::Pos2 { x, y: y1 }, + }; + + if ui.rect_contains_pointer(frame_rect) { + hovered_frame = Some((i, frame_rect)) + } + + if i != num_frames - 1 { + ui.painter().vline(x, egui::Rangef { min: y0, max: y1 }, separator); + } + prev_x = x; + } + + let selected_cell_idx = *current_frame - first_frame; + + if num_frames > 1 { + let x0 = min.x + selected_cell_idx as f32 * size.x / n; + let x1 = min.x + (selected_cell_idx + 1) as f32 * size.x / n; + let selected_frame_rect = egui::Rect { + min: egui::Pos2 { x: x0, y: y0 }, + max: egui::Pos2 { x: x1, y: y1 }, + }; + ui.painter().rect( + selected_frame_rect, + 0u8, + style.visuals.widgets.active.bg_fill, + style.visuals.widgets.active.bg_stroke, + egui::StrokeKind::Inside, + ); + } + + if num_frames > 0 { + if let Some((idx, frame_rect)) = hovered_frame { + ui.painter().rect( + frame_rect, 0u8, + if idx == selected_cell_idx { + style.visuals.widgets.active.bg_fill + } else { + style.visuals.widgets.hovered.bg_fill + }, + style.visuals.widgets.hovered.bg_stroke, + egui::StrokeKind::Inside, + ); + + if response.clicked() || response.is_pointer_button_down_on() { + *current_frame = first_frame + idx; + } + } + } + }); +}