better already
This commit is contained in:
@@ -2,6 +2,7 @@ use minifb::{Key, Window, WindowOptions};
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use rand::Rng;
|
||||
use teleprof::instrument;
|
||||
|
||||
@@ -9,6 +10,8 @@ const WIDTH: usize = 800;
|
||||
const HEIGHT: usize = 600;
|
||||
const BALL_RADIUS: usize = 20;
|
||||
|
||||
static COLOR_PICKER_COUNTER: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
struct Ball {
|
||||
x: f32,
|
||||
y: f32,
|
||||
@@ -94,8 +97,9 @@ fn main_frame(
|
||||
// If we hit a wall, spawn a thread to pick a new color
|
||||
if hit_wall {
|
||||
let ball_clone = Arc::clone(ball);
|
||||
let id = COLOR_PICKER_COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
thread::spawn(move || {
|
||||
teleprof::set_thread_name("ColorPicker");
|
||||
teleprof::set_thread_name(format!("ColorPicker-{}", id));
|
||||
pick_new_color(ball_clone);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@ fn main() {
|
||||
// Start the telemetry window
|
||||
teleprof::start();
|
||||
|
||||
// Name the main thread
|
||||
teleprof::set_thread_name("Main");
|
||||
|
||||
println!("Teleprof demo running...");
|
||||
println!("Press Space in the profiler window to pause/unpause");
|
||||
println!("Mouse wheel to zoom, drag to pan");
|
||||
@@ -44,6 +47,7 @@ fn physics_update() {
|
||||
// Spawn some worker threads
|
||||
let handles: Vec<_> = (0..3).map(|i| {
|
||||
thread::spawn(move || {
|
||||
teleprof::set_thread_name(format!("Physics-{}", i));
|
||||
physics_worker(i);
|
||||
})
|
||||
}).collect();
|
||||
|
||||
@@ -144,15 +144,14 @@ mod window {
|
||||
const MAX_EVENTS: usize = 1_000_000;
|
||||
|
||||
// Monokai palette
|
||||
const COLORS: [u32; 8] = [
|
||||
const COLORS: [u32; 7] = [
|
||||
0x75715E, // Gray
|
||||
0xF92672, // Pink
|
||||
0xA6E22E, // Green
|
||||
0xFD971F, // Orange
|
||||
0x66D9EF, // Cyan
|
||||
0xAE81FF, // Purple
|
||||
0xE6DB74, // Yellow
|
||||
0xF8F8F2, // White
|
||||
0x75715E, // Gray
|
||||
];
|
||||
|
||||
const BG_COLOR: u32 = 0x272822;
|
||||
@@ -230,6 +229,10 @@ mod window {
|
||||
timeline_time_offset: f64,
|
||||
timeline_time_scale: f64,
|
||||
|
||||
// Vertical scroll
|
||||
icicle_scroll_y: f32,
|
||||
timeline_scroll_y: f32,
|
||||
|
||||
// Mouse state
|
||||
mouse_down: bool,
|
||||
last_mouse_x: f32,
|
||||
@@ -238,11 +241,26 @@ mod window {
|
||||
mouse_x: f32,
|
||||
mouse_y: f32,
|
||||
|
||||
// Box selection for zoom
|
||||
selecting: bool,
|
||||
selection_start_x: f32,
|
||||
selection_end_x: f32,
|
||||
|
||||
// Hover state
|
||||
hovered_span: Option<HoveredSpan>,
|
||||
|
||||
// Pause state
|
||||
paused: bool,
|
||||
pause_guard: Option<std::sync::MutexGuard<'static, ()>>,
|
||||
}
|
||||
|
||||
struct HoveredSpan {
|
||||
name: &'static str,
|
||||
duration_us: f64,
|
||||
thread_id: u64,
|
||||
start_time: f64,
|
||||
}
|
||||
|
||||
impl ViewState {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
@@ -250,12 +268,18 @@ mod window {
|
||||
icicle_time_scale: 100.0,
|
||||
timeline_time_offset: 0.0,
|
||||
timeline_time_scale: 100.0,
|
||||
icicle_scroll_y: 0.0,
|
||||
timeline_scroll_y: 0.0,
|
||||
mouse_down: false,
|
||||
last_mouse_x: 0.0,
|
||||
last_mouse_y: 0.0,
|
||||
mouse_visible: false,
|
||||
mouse_x: 0.0,
|
||||
mouse_y: 0.0,
|
||||
selecting: false,
|
||||
selection_start_x: 0.0,
|
||||
selection_end_x: 0.0,
|
||||
hovered_span: None,
|
||||
paused: false,
|
||||
pause_guard: None,
|
||||
}
|
||||
@@ -291,8 +315,16 @@ mod window {
|
||||
|
||||
let (width, height) = window.get_size();
|
||||
|
||||
// Collect spans for this frame
|
||||
let spans: Vec<_> = buffer.iter().collect();
|
||||
let earliest = if !spans.is_empty() {
|
||||
spans.iter().map(|s| s.start).min().unwrap()
|
||||
} else {
|
||||
Instant::now()
|
||||
};
|
||||
|
||||
// Handle input
|
||||
handle_input(&mut window, &mut view, width, height);
|
||||
handle_input(&mut window, &mut view, &buffer, earliest, width, height);
|
||||
|
||||
// Resize framebuffer if needed
|
||||
if framebuffer.len() != width * height {
|
||||
@@ -310,7 +342,7 @@ mod window {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_input(window: &mut Window, view: &mut ViewState, _width: usize, height: usize) {
|
||||
fn handle_input(window: &mut Window, view: &mut ViewState, buffer: &RingBuffer, earliest: Instant, _width: usize, height: usize) {
|
||||
// Pause/unpause
|
||||
if window.is_key_pressed(Key::Space, minifb::KeyRepeat::No) {
|
||||
view.paused = !view.paused;
|
||||
@@ -328,47 +360,128 @@ mod window {
|
||||
view.mouse_y = my;
|
||||
|
||||
let icicle_height = height / 2;
|
||||
let _is_icicle = my < icicle_height as f32;
|
||||
let is_icicle = my < icicle_height as f32;
|
||||
|
||||
// Handle mouse wheel (zoom)
|
||||
// Check if shift is pressed (for timeline zoom)
|
||||
let shift_pressed = window.is_key_down(Key::LeftShift) || window.is_key_down(Key::RightShift);
|
||||
|
||||
// Handle mouse wheel
|
||||
if let Some((_, scroll_y)) = window.get_scroll_wheel() {
|
||||
if scroll_y != 0.0 {
|
||||
if shift_pressed && !is_icicle {
|
||||
// Shift + scroll in timeline = zoom timeline only
|
||||
let zoom_factor = if scroll_y > 0.0 { 1.2 } else { 1.0 / 1.2 };
|
||||
let old_scale = view.timeline_time_scale;
|
||||
let new_scale = old_scale * zoom_factor;
|
||||
let mouse_time = view.timeline_time_offset + (mx as f64 / old_scale);
|
||||
|
||||
// Synchronize both views - zoom changes scale for both
|
||||
view.timeline_time_scale = new_scale;
|
||||
view.timeline_time_offset = mouse_time - (mx as f64 / new_scale);
|
||||
|
||||
// Also update icicle to match
|
||||
view.icicle_time_scale = new_scale;
|
||||
view.icicle_time_offset = view.timeline_time_offset;
|
||||
} else if !shift_pressed && scroll_y.abs() > 0.5 {
|
||||
// Regular scroll = zoom both views (horizontal)
|
||||
let zoom_factor = if scroll_y > 0.0 { 1.2 } else { 1.0 / 1.2 };
|
||||
let old_scale = view.icicle_time_scale;
|
||||
let new_scale = old_scale * zoom_factor;
|
||||
|
||||
// Calculate mouse time in world space (same for both views since synchronized)
|
||||
let mouse_time = view.icicle_time_offset + (mx as f64 / old_scale);
|
||||
|
||||
// Update scale and adjust offset to keep mouse position stable
|
||||
view.icicle_time_scale = new_scale;
|
||||
view.timeline_time_scale = new_scale;
|
||||
|
||||
let new_offset = mouse_time - (mx as f64 / new_scale);
|
||||
view.icicle_time_offset = new_offset;
|
||||
view.timeline_time_offset = new_offset;
|
||||
} else {
|
||||
// Vertical scroll
|
||||
let scroll_amount = scroll_y * 20.0;
|
||||
if is_icicle {
|
||||
view.icicle_scroll_y = (view.icicle_scroll_y - scroll_amount).max(0.0);
|
||||
} else {
|
||||
view.timeline_scroll_y = (view.timeline_scroll_y - scroll_amount).max(0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle mouse drag (pan)
|
||||
let mouse_down = window.get_mouse_down(MouseButton::Left);
|
||||
// Handle mouse buttons
|
||||
let left_down = window.get_mouse_down(MouseButton::Left);
|
||||
let right_down = window.get_mouse_down(MouseButton::Right);
|
||||
|
||||
if mouse_down && view.mouse_down {
|
||||
// Right click = pan
|
||||
if right_down && view.mouse_down {
|
||||
let dx = mx - view.last_mouse_x;
|
||||
|
||||
// Pan both views together (synchronized)
|
||||
let delta = dx as f64 / view.icicle_time_scale;
|
||||
view.icicle_time_offset -= delta;
|
||||
view.timeline_time_offset -= delta;
|
||||
}
|
||||
|
||||
view.mouse_down = mouse_down;
|
||||
// Left click = box selection for zoom
|
||||
if left_down && !view.selecting && !view.mouse_down {
|
||||
view.selecting = true;
|
||||
view.selection_start_x = mx;
|
||||
view.selection_end_x = mx;
|
||||
} else if left_down && view.selecting {
|
||||
view.selection_end_x = mx;
|
||||
} else if !left_down && view.selecting {
|
||||
// Complete selection - zoom to selected region
|
||||
let x1 = view.selection_start_x.min(view.selection_end_x);
|
||||
let x2 = view.selection_start_x.max(view.selection_end_x);
|
||||
|
||||
if (x2 - x1) > 5.0 { // Minimum selection size
|
||||
let time1 = view.icicle_time_offset + (x1 as f64 / view.icicle_time_scale);
|
||||
let time2 = view.icicle_time_offset + (x2 as f64 / view.icicle_time_scale);
|
||||
let time_range = time2 - time1;
|
||||
|
||||
// Calculate new scale to fit selection in view
|
||||
let new_scale = window.get_size().0 as f64 / time_range;
|
||||
view.icicle_time_scale = new_scale;
|
||||
view.timeline_time_scale = new_scale;
|
||||
view.icicle_time_offset = time1;
|
||||
view.timeline_time_offset = time1;
|
||||
}
|
||||
|
||||
view.selecting = false;
|
||||
}
|
||||
|
||||
// Update hover detection
|
||||
update_hover(view, buffer, earliest, mx, my, icicle_height);
|
||||
|
||||
view.mouse_down = left_down || right_down;
|
||||
view.last_mouse_x = mx;
|
||||
view.last_mouse_y = my;
|
||||
} else {
|
||||
view.mouse_visible = false;
|
||||
view.hovered_span = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn update_hover(view: &mut ViewState, buffer: &RingBuffer, earliest: Instant, mx: f32, my: f32, icicle_height: usize) {
|
||||
view.hovered_span = None;
|
||||
|
||||
if my >= icicle_height as f32 {
|
||||
return; // Only hover in icicle view for now
|
||||
}
|
||||
|
||||
let mouse_time = view.icicle_time_offset + (mx as f64 / view.icicle_time_scale);
|
||||
|
||||
// Find span under cursor
|
||||
for span in buffer.iter() {
|
||||
let start_time = (span.start - earliest).as_secs_f64();
|
||||
let end_time = (span.end - earliest).as_secs_f64();
|
||||
|
||||
if mouse_time >= start_time && mouse_time <= end_time {
|
||||
let duration_us = (end_time - start_time) * 1_000_000.0;
|
||||
view.hovered_span = Some(HoveredSpan {
|
||||
name: span.name,
|
||||
duration_us,
|
||||
thread_id: span.thread_id,
|
||||
start_time,
|
||||
});
|
||||
break; // Take first match (could be improved to find best match by y-position)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -412,31 +525,189 @@ mod window {
|
||||
font,
|
||||
);
|
||||
|
||||
// Draw cursor
|
||||
if view.mouse_visible {
|
||||
let cursor_x = view.mouse_x as usize;
|
||||
// Draw timestamp axis at the bottom of icicle view
|
||||
draw_timestamp_axis(framebuffer, width, icicle_height - 20, view, earliest, font);
|
||||
|
||||
// Draw selection box if selecting
|
||||
if view.selecting {
|
||||
let x1 = view.selection_start_x.min(view.selection_end_x) as usize;
|
||||
let x2 = view.selection_start_x.max(view.selection_end_x) as usize;
|
||||
let selection_color = 0x4080FF; // Semi-transparent blue
|
||||
|
||||
// Draw vertical line
|
||||
for y in 0..height {
|
||||
if cursor_x < width {
|
||||
let idx = y * width + cursor_x;
|
||||
for x in x1..x2.min(width) {
|
||||
let idx = y * width + x;
|
||||
if idx < framebuffer.len() {
|
||||
// Make cursor semi-transparent by blending
|
||||
let bg = framebuffer[idx];
|
||||
let bg_r = ((bg >> 16) & 0xFF) as u32;
|
||||
let bg_g = ((bg >> 8) & 0xFF) as u32;
|
||||
let bg_b = (bg & 0xFF) as u32;
|
||||
let bg_r = ((bg >> 16) & 0xFF) as f32;
|
||||
let bg_g = ((bg >> 8) & 0xFF) as f32;
|
||||
let bg_b = (bg & 0xFF) as f32;
|
||||
|
||||
let alpha = 0.5;
|
||||
let r = (bg_r as f32 * (1.0 - alpha) + 255.0 * alpha) as u32;
|
||||
let g = (bg_g as f32 * (1.0 - alpha) + 255.0 * alpha) as u32;
|
||||
let b = (bg_b as f32 * (1.0 - alpha) + 255.0 * alpha) as u32;
|
||||
let sel_r = ((selection_color >> 16) & 0xFF) as f32;
|
||||
let sel_g = ((selection_color >> 8) & 0xFF) as f32;
|
||||
let sel_b = (selection_color & 0xFF) as f32;
|
||||
|
||||
let alpha = 0.3;
|
||||
let r = (bg_r * (1.0 - alpha) + sel_r * alpha) as u32;
|
||||
let g = (bg_g * (1.0 - alpha) + sel_g * alpha) as u32;
|
||||
let b = (bg_b * (1.0 - alpha) + sel_b * alpha) as u32;
|
||||
|
||||
framebuffer[idx] = (r << 16) | (g << 8) | b;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw crosshair cursor
|
||||
if view.mouse_visible && !view.selecting {
|
||||
let cursor_x = view.mouse_x as usize;
|
||||
let cursor_y = view.mouse_y as usize;
|
||||
|
||||
// Vertical line
|
||||
for y in 0..height {
|
||||
if cursor_x < width {
|
||||
let idx = y * width + cursor_x;
|
||||
if idx < framebuffer.len() {
|
||||
framebuffer[idx] = blend_cursor(framebuffer[idx]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Horizontal line
|
||||
if cursor_y < height {
|
||||
for x in 0..width {
|
||||
let idx = cursor_y * width + x;
|
||||
if idx < framebuffer.len() {
|
||||
framebuffer[idx] = blend_cursor(framebuffer[idx]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw hover tooltip
|
||||
if let Some(ref hover) = view.hovered_span {
|
||||
draw_tooltip(framebuffer, width, height, view, hover, font);
|
||||
}
|
||||
}
|
||||
|
||||
fn blend_cursor(bg: u32) -> u32 {
|
||||
let bg_r = ((bg >> 16) & 0xFF) as f32;
|
||||
let bg_g = ((bg >> 8) & 0xFF) as f32;
|
||||
let bg_b = (bg & 0xFF) as f32;
|
||||
|
||||
let alpha = 0.6;
|
||||
let r = (bg_r * (1.0 - alpha) + 255.0 * alpha) as u32;
|
||||
let g = (bg_g * (1.0 - alpha) + 255.0 * alpha) as u32;
|
||||
let b = (bg_b * (1.0 - alpha) + 255.0 * alpha) as u32;
|
||||
|
||||
(r << 16) | (g << 8) | b
|
||||
}
|
||||
|
||||
fn draw_tooltip(
|
||||
framebuffer: &mut [u32],
|
||||
width: usize,
|
||||
height: usize,
|
||||
view: &ViewState,
|
||||
hover: &HoveredSpan,
|
||||
font: &fontdue::Font,
|
||||
) {
|
||||
let tooltip_w = 280;
|
||||
let tooltip_h = 80;
|
||||
let padding = 8;
|
||||
|
||||
// Position tooltip near cursor but keep it on screen
|
||||
let tooltip_x = (view.mouse_x as usize + 20).min(width.saturating_sub(tooltip_w + 10));
|
||||
let tooltip_y = (view.mouse_y as usize + 20).min(height.saturating_sub(tooltip_h + 10));
|
||||
|
||||
// Draw tooltip background
|
||||
for dy in 0..tooltip_h {
|
||||
for dx in 0..tooltip_w {
|
||||
let x = tooltip_x + dx;
|
||||
let y = tooltip_y + dy;
|
||||
if x < width && y < height {
|
||||
let idx = y * width + x;
|
||||
if idx < framebuffer.len() {
|
||||
// Dark background with border
|
||||
let color = if dx == 0 || dy == 0 || dx == tooltip_w - 1 || dy == tooltip_h - 1 {
|
||||
0x808080 // Border
|
||||
} else {
|
||||
0x1E1E1E // Background
|
||||
};
|
||||
framebuffer[idx] = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw text
|
||||
let text_x = tooltip_x + padding;
|
||||
let text_y = tooltip_y + padding;
|
||||
|
||||
draw_text(framebuffer, width, text_x, text_y, hover.name, font, 14.0, 0xFFFFFF);
|
||||
draw_text(framebuffer, width, text_x, text_y + 20,
|
||||
&format!("Duration: {:.2} μs", hover.duration_us), font, 12.0, 0xCCCCCC);
|
||||
draw_text(framebuffer, width, text_x, text_y + 38,
|
||||
&format!("Thread: {}", hover.thread_id), font, 12.0, 0xCCCCCC);
|
||||
draw_text(framebuffer, width, text_x, text_y + 56,
|
||||
&format!("Start: {:.6} s", hover.start_time), font, 12.0, 0xCCCCCC);
|
||||
}
|
||||
|
||||
fn draw_timestamp_axis(
|
||||
framebuffer: &mut [u32],
|
||||
width: usize,
|
||||
y: usize,
|
||||
view: &ViewState,
|
||||
_earliest: Instant,
|
||||
font: &fontdue::Font,
|
||||
) {
|
||||
// Draw background bar
|
||||
for x in 0..width {
|
||||
let idx = y * width + x;
|
||||
if idx < framebuffer.len() {
|
||||
framebuffer[idx] = 0x1E1E1E;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate time markers
|
||||
let visible_duration = width as f64 / view.icicle_time_scale;
|
||||
let time_step = calculate_time_step(visible_duration);
|
||||
|
||||
let start_time = view.icicle_time_offset;
|
||||
let first_marker = (start_time / time_step).ceil() * time_step;
|
||||
|
||||
let mut time = first_marker;
|
||||
while time < start_time + visible_duration {
|
||||
let x = ((time - view.icicle_time_offset) * view.icicle_time_scale) as i32;
|
||||
|
||||
if x >= 0 && x < width as i32 {
|
||||
// Draw tick mark
|
||||
for dy in 0..6 {
|
||||
let idx = (y + dy) * width + x as usize;
|
||||
if idx < framebuffer.len() {
|
||||
framebuffer[idx] = 0x808080;
|
||||
}
|
||||
}
|
||||
|
||||
// Draw time label
|
||||
let label = format!("{:.3}s", time);
|
||||
draw_text(framebuffer, width, (x + 2).max(0) as usize, y + 6, &label, font, 10.0, 0xCCCCCC);
|
||||
}
|
||||
|
||||
time += time_step;
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_time_step(visible_duration: f64) -> f64 {
|
||||
// Choose appropriate time step based on visible duration
|
||||
let steps = [0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0];
|
||||
|
||||
for &step in &steps {
|
||||
if visible_duration / step < 20.0 {
|
||||
return step;
|
||||
}
|
||||
}
|
||||
|
||||
10.0
|
||||
}
|
||||
|
||||
fn render_icicle(
|
||||
@@ -460,11 +731,16 @@ mod window {
|
||||
}
|
||||
}
|
||||
|
||||
// Sort roots by start time
|
||||
roots.sort_by_key(|s| s.start);
|
||||
|
||||
// Render each root and its children recursively
|
||||
let row_height = 24;
|
||||
let mut y_offset = 0;
|
||||
let mut y_offset = view.icicle_scroll_y as usize;
|
||||
|
||||
for root in roots {
|
||||
// Only render if in view
|
||||
if y_offset < height {
|
||||
y_offset = render_icicle_span(
|
||||
framebuffer,
|
||||
width,
|
||||
@@ -476,7 +752,11 @@ mod window {
|
||||
row_height,
|
||||
view,
|
||||
font,
|
||||
0, // depth
|
||||
);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -491,6 +771,7 @@ mod window {
|
||||
row_height: usize,
|
||||
view: &ViewState,
|
||||
font: &fontdue::Font,
|
||||
depth: usize,
|
||||
) -> usize {
|
||||
if y + row_height > height {
|
||||
return y;
|
||||
@@ -506,36 +787,43 @@ mod window {
|
||||
// Only render if visible
|
||||
if x2 > 0 && x1 < width as i32 {
|
||||
let color = get_color_for_name(span.name);
|
||||
let bar_width = (x2 - x1).max(1) as usize;
|
||||
|
||||
fill_rect(framebuffer, width, x1.max(0) as usize, y, (x2 - x1).max(1) as usize, row_height - 2, color);
|
||||
fill_rect(framebuffer, width, x1.max(0) as usize, y, bar_width, row_height - 2, color);
|
||||
|
||||
// Render text if there's enough space
|
||||
let text_width = (x2 - x1) as usize;
|
||||
if text_width > 40 {
|
||||
// Render text if there's enough space, otherwise render to the right
|
||||
if bar_width > 40 {
|
||||
draw_text(framebuffer, width, x1.max(0) as usize + 4, y + 4, span.name, font, 14.0, TEXT_COLOR);
|
||||
} else if x2 >= 0 && x2 < width as i32 {
|
||||
// Draw text to the right of the bar
|
||||
draw_text(framebuffer, width, x2.max(0) as usize + 4, y + 4, span.name, font, 14.0, TEXT_COLOR);
|
||||
}
|
||||
}
|
||||
|
||||
// Render children
|
||||
let mut next_y = y + row_height;
|
||||
// Render children at next depth level
|
||||
let mut child_y = y + row_height;
|
||||
if let Some(child_spans) = children.get(&span.span_id) {
|
||||
for child in child_spans {
|
||||
next_y = render_icicle_span(
|
||||
if child_y >= height {
|
||||
break;
|
||||
}
|
||||
child_y = render_icicle_span(
|
||||
framebuffer,
|
||||
width,
|
||||
height,
|
||||
child,
|
||||
children,
|
||||
earliest,
|
||||
next_y,
|
||||
child_y,
|
||||
row_height,
|
||||
view,
|
||||
font,
|
||||
depth + 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
next_y
|
||||
child_y
|
||||
}
|
||||
|
||||
fn render_timeline(
|
||||
|
||||
Reference in New Issue
Block a user