From 09457e56d45f9b7ea9a7cb6f7df5b0c9cdad0ae7 Mon Sep 17 00:00:00 2001 From: Mark Kalsbeek Date: Wed, 17 Dec 2025 00:18:08 +0100 Subject: [PATCH] big rework, combine timeline and icicle view into hybrid, itteratively build tree structure for perfmance and stability --- shell.nix | 4 + teleprof/src/lib.rs | 1437 ++++++++++++++++++++++++++----------------- 2 files changed, 865 insertions(+), 576 deletions(-) diff --git a/shell.nix b/shell.nix index 1b52abb..26c68ef 100644 --- a/shell.nix +++ b/shell.nix @@ -11,6 +11,10 @@ let # Wayland support wayland libxkbcommon + + # Rust + rust-analyzer + ]; in mkShell { diff --git a/teleprof/src/lib.rs b/teleprof/src/lib.rs index d0285cd..d6700d3 100644 --- a/teleprof/src/lib.rs +++ b/teleprof/src/lib.rs @@ -45,25 +45,20 @@ pub struct SpanGuard { impl SpanGuard { pub fn new(name: &'static str) -> Self { // Handle pause mechanism BEFORE creating the span - // This way the pause time isn't counted in the span if let Err(_guard) = PAUSE.try_lock() { - // We're paused - block until unpaused - // The _guard will be dropped when we acquire the lock, releasing it immediately let _pause_lock = PAUSE.lock().unwrap(); - // Now we're unpaused, continue with span creation } let thread_id = std::thread::current().id(); let parent_id = PARENT_SPAN.with(|p| p.get()); let parent_name = PARENT_SPAN_NAME.with(|p| p.get()); - // Check if this is a duplicate span (parent has same name) + // Check if this is a duplicate span let is_duplicate = parent_name == Some(name); if is_duplicate { - // Don't create a new span, just pass through return Self { - span_id: 0, // Dummy value, won't be used + span_id: 0, previous_parent: parent_id, previous_parent_name: parent_name, name, @@ -72,8 +67,6 @@ impl SpanGuard { } let span_id = next_span_id(); - - // Store the previous parent so we can restore it when this span ends let previous_parent = parent_id; let previous_parent_name = parent_name; @@ -101,7 +94,6 @@ impl SpanGuard { impl Drop for SpanGuard { fn drop(&mut self) { if self.is_duplicate { - // Don't send end event or modify thread-local state return; } @@ -110,7 +102,6 @@ impl Drop for SpanGuard { timestamp: Instant::now(), }).ok(); - // Restore the previous parent (grandparent of this span) PARENT_SPAN.with(|p| p.set(self.previous_parent)); PARENT_SPAN_NAME.with(|p| p.set(self.previous_parent_name)); } @@ -190,6 +181,9 @@ mod window { const INITIAL_WIDTH: usize = 1280; const INITIAL_HEIGHT: usize = 720; const MAX_EVENTS: usize = 1_000_000; + const TRACK_HEADER_HEIGHT: usize = 24; + const COLLAPSED_TRACK_HEIGHT: usize = 24; + const ROW_HEIGHT: usize = 20; // Monokai palette const COLORS: [u32; 7] = [ @@ -204,6 +198,86 @@ mod window { const BG_COLOR: u32 = 0x272822; const TEXT_COLOR: u32 = 0xF8F8F2; + const HEADER_BG: u32 = 0x1E1E1E; + const ACTIVE_COLOR: u32 = 0x4080FF; + + // Tree node representing a span in the call stack + #[derive(Clone)] + struct SpanNode { + span_id: u64, + parent_id: Option, // Need this for reparenting + name: &'static str, + start: Instant, + end: Option, // None = ongoing + children: Vec, + } + + impl SpanNode { + fn max_depth(&self) -> usize { + if self.children.is_empty() { + 1 + } else { + 1 + self.children.iter().map(|c| c.max_depth()).max().unwrap_or(0) + } + } + } + + // A track represents one thread's timeline + struct ThreadTrack { + thread_id: u64, + name: Option, + root_spans: Vec, + expanded: bool, + active_intervals: Vec<(Instant, Instant)>, // When thread was active + } + + impl ThreadTrack { + fn max_depth(&self) -> usize { + self.root_spans.iter().map(|n| n.max_depth()).max().unwrap_or(0) + } + + fn height(&self) -> usize { + if self.expanded { + TRACK_HEADER_HEIGHT + self.max_depth() * ROW_HEIGHT + } else { + TRACK_HEADER_HEIGHT + COLLAPSED_TRACK_HEIGHT + } + } + } + + // Parsed frame data - rebuilt each frame + struct FrameData { + tracks: Vec, + earliest: Instant, + latest: Instant, + } + + // Persistent state for incremental tree building + struct TreeCache { + // Per-thread trees + thread_trees: HashMap>, + // Track which span_ids we've already processed + processed_spans: std::collections::HashSet, + // Track last known event count to detect new events + last_event_count: usize, + } + + impl TreeCache { + fn new() -> Self { + Self { + thread_trees: HashMap::new(), + processed_spans: std::collections::HashSet::new(), + last_event_count: 0, + } + } + } + + struct RingBuffer { + spans: Vec, + head: usize, + pending_starts: HashMap, u64, &'static str, Instant)>, + thread_names: HashMap, + } struct CompletedSpan { span_id: u64, @@ -214,13 +288,6 @@ mod window { end: Instant, } - struct RingBuffer { - spans: Vec, - head: usize, - pending_starts: HashMap, u64, &'static str, Instant)>, - thread_names: HashMap, - } - impl RingBuffer { fn new() -> Self { Self { @@ -268,18 +335,19 @@ mod window { Box::new(self.spans[self.head..].iter().chain(self.spans[..self.head].iter())) } } + + fn get_ongoing_spans(&self) -> Vec<(u64, Option, u64, &'static str, Instant)> { + self.pending_starts.values().cloned().collect() + } } struct ViewState { - // Pan/zoom (synchronized between views) - icicle_time_offset: f64, - icicle_time_scale: f64, - timeline_time_offset: f64, - timeline_time_scale: f64, + // Pan/zoom (unified) + time_offset: f64, + time_scale: f64, // Vertical scroll - icicle_scroll_y: f32, - timeline_scroll_y: f32, + scroll_y: f32, // Mouse state mouse_down: bool, @@ -300,6 +368,10 @@ mod window { // Pause state paused: bool, pause_guard: Option>, + pause_timestamp: Option, // Time when pause was activated + + // Track expansion state (per thread_id) + track_expanded: HashMap, } struct HoveredSpan { @@ -307,17 +379,15 @@ mod window { duration_us: f64, thread_id: u64, start_time: f64, + ongoing: bool, } impl ViewState { fn new() -> Self { Self { - icicle_time_offset: 0.0, - 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, + time_offset: 0.0, + time_scale: 100.0, + scroll_y: 0.0, mouse_down: false, last_mouse_x: 0.0, last_mouse_y: 0.0, @@ -330,8 +400,23 @@ mod window { hovered_span: None, paused: false, pause_guard: None, + pause_timestamp: None, + track_expanded: HashMap::new(), } } + + fn current_time(&self) -> Instant { + self.pause_timestamp.unwrap_or_else(Instant::now) + } + + fn is_track_expanded(&self, thread_id: u64) -> bool { + *self.track_expanded.get(&thread_id).unwrap_or(&true) + } + + fn toggle_track(&mut self, thread_id: u64) { + let expanded = self.is_track_expanded(thread_id); + self.track_expanded.insert(thread_id, !expanded); + } } pub fn run() -> Result<(), Box> { @@ -350,6 +435,7 @@ mod window { let mut buffer = RingBuffer::new(); let mut view = ViewState::new(); let mut framebuffer = vec![BG_COLOR; INITIAL_WIDTH * INITIAL_HEIGHT]; + let mut tree_cache = TreeCache::new(); // Load font let font_data = include_bytes!("../../fonts/DejaVuSansMono.ttf"); @@ -363,16 +449,11 @@ 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() - }; + // Parse ringbuffer into frame data (incrementally) + let frame_data = parse_frame_data(&buffer, &view, &mut tree_cache); // Handle input - handle_input(&mut window, &mut view, &buffer, earliest, width, height); + handle_input(&mut window, &mut view, &frame_data, width, height); // Resize framebuffer if needed if framebuffer.len() != width * height { @@ -382,22 +463,327 @@ mod window { framebuffer.fill(BG_COLOR); // Render - render_frame(&mut framebuffer, width, height, &buffer, &view, &font); + render_frame(&mut framebuffer, width, height, &frame_data, &view, &font); window.update_with_buffer(&framebuffer, width, height)?; } Ok(()) } + + // Parse ringbuffer into per-thread trees (incrementally) + fn parse_frame_data(buffer: &RingBuffer, view: &ViewState, cache: &mut TreeCache) -> FrameData { + let completed_spans: Vec<_> = buffer.iter().collect(); + let ongoing_spans = buffer.get_ongoing_spans(); + + if completed_spans.is_empty() && ongoing_spans.is_empty() { + return FrameData { + tracks: Vec::new(), + earliest: Instant::now(), + latest: Instant::now(), + }; + } + + // Check if we have new events to process + let current_event_count = completed_spans.len(); + let has_new_events = current_event_count != cache.last_event_count; + + if has_new_events { + // Process only new spans + for span in &completed_spans { + if !cache.processed_spans.contains(&span.span_id) { + add_span_to_tree(span, &mut cache.thread_trees); + cache.processed_spans.insert(span.span_id); + } + } + cache.last_event_count = current_event_count; + } + + // Find time range + let earliest = completed_spans.iter() + .map(|s| s.start) + .chain(ongoing_spans.iter().map(|(_, _, _, _, start)| *start)) + .min() + .unwrap_or_else(Instant::now); + + let latest = completed_spans.iter() + .map(|s| s.end) + .max() + .unwrap_or(earliest); + + // Get all thread IDs (from cache and ongoing) + let mut thread_ids: Vec<_> = cache.thread_trees.keys() + .copied() + .collect(); + + for (_, _, thread_id, _, _) in &ongoing_spans { + if !thread_ids.contains(thread_id) { + thread_ids.push(*thread_id); + } + } + + thread_ids.sort_unstable(); + + let mut tracks = Vec::new(); + let current_time = view.current_time(); + + // Group ongoing spans by thread + let mut thread_ongoing: HashMap, &'static str, Instant)>> = HashMap::new(); + for (span_id, parent_id, thread_id, name, start) in ongoing_spans { + thread_ongoing.entry(thread_id).or_default().push((span_id, parent_id, name, start)); + } + + for thread_id in thread_ids { + // Get cached tree for this thread (clone so we can modify it) + let mut root_spans = cache.thread_trees.get(&thread_id) + .cloned() + .unwrap_or_default(); + + // Add ongoing spans to the tree FIRST (so they can be parents) + let ongoing = thread_ongoing.get(&thread_id).map(|v| v.as_slice()).unwrap_or(&[]); + if !ongoing.is_empty() { + add_ongoing_spans_to_tree(&mut root_spans, ongoing); + + // Now reparent any cached roots that have ongoing parents + reparent_to_ongoing(&mut root_spans); + } + + // Calculate active intervals + let completed = completed_spans.iter() + .filter(|s| s.thread_id == thread_id) + .copied() + .collect::>(); + let active_intervals = calculate_active_intervals(&completed, ongoing, current_time); + + tracks.push(ThreadTrack { + thread_id, + name: buffer.thread_names.get(&thread_id).cloned(), + root_spans, + expanded: view.is_track_expanded(thread_id), + active_intervals, + }); + } + + FrameData { + tracks, + earliest, + latest, + } + } + + // Add a single completed span to the tree cache + fn add_span_to_tree(span: &CompletedSpan, thread_trees: &mut HashMap>) { + let node = SpanNode { + span_id: span.span_id, + parent_id: span.parent_id, + name: span.name, + start: span.start, + end: Some(span.end), + children: Vec::new(), + }; + + let roots = thread_trees.entry(span.thread_id).or_default(); + + if let Some(parent_id) = span.parent_id { + // Try to find parent and add as child + if !try_add_child_to_parent(roots, parent_id, node.clone()) { + // Parent not found yet, add as root (will be reparented later) + roots.push(node); + } + } else { + // No parent, add as root + roots.push(node); + } + + // Try to reparent any existing roots that should be children of this span + reparent_orphans(roots, span.span_id); + + // Sort roots and children by start time + roots.sort_by_key(|n| n.start); + sort_tree_children(roots); + } + + // Recursively try to add a node as a child of the given parent_id + fn try_add_child_to_parent(nodes: &mut [SpanNode], parent_id: u64, child: SpanNode) -> bool { + for node in nodes { + if node.span_id == parent_id { + node.children.push(child); + node.children.sort_by_key(|n| n.start); + return true; + } + if try_add_child_to_parent(&mut node.children, parent_id, child.clone()) { + return true; + } + } + false + } + + // Find nodes at root level that should be children of span_id and reparent them + fn reparent_orphans(roots: &mut Vec, span_id: u64) { + let mut to_reparent = Vec::new(); + + // Find roots that should be children + for i in (0..roots.len()).rev() { + if let Some(parent_id) = get_parent_id_from_metadata(&roots[i]) { + if parent_id == span_id { + to_reparent.push(roots.remove(i)); + } + } + } + + // Add them as children + for orphan in to_reparent { + try_add_child_to_parent(roots, span_id, orphan); + } + } + + // Helper to get parent_id from SpanNode + fn get_parent_id_from_metadata(node: &SpanNode) -> Option { + node.parent_id + } + + fn sort_tree_children(nodes: &mut [SpanNode]) { + for node in nodes { + node.children.sort_by_key(|n| n.start); + sort_tree_children(&mut node.children); + } + } + + // Add ongoing spans to an existing tree (temporarily, not persisted to cache) + fn add_ongoing_spans_to_tree(roots: &mut Vec, ongoing: &[(u64, Option, &'static str, Instant)]) { + for &(span_id, parent_id, name, start) in ongoing { + let node = SpanNode { + span_id, + parent_id, + name, + start, + end: None, + children: Vec::new(), + }; + + if let Some(parent_id) = parent_id { + if !try_add_child_to_parent(roots, parent_id, node.clone()) { + roots.push(node); + } + } else { + roots.push(node); + } + } + + roots.sort_by_key(|n| n.start); + sort_tree_children(roots); + } + + // Reparent root nodes that should be children of ongoing spans + fn reparent_to_ongoing(roots: &mut Vec) { + // Find all ongoing span IDs (they have end: None) + let ongoing_ids: Vec = collect_ongoing_ids(roots); + + // For each ongoing span, check if any roots should be its children + for ongoing_id in ongoing_ids { + let mut to_reparent = Vec::new(); + + // Find roots that claim this ongoing span as parent + for i in (0..roots.len()).rev() { + if let Some(parent_id) = roots[i].parent_id { + if parent_id == ongoing_id && roots[i].end.is_some() { + // This is a completed span that should be a child + to_reparent.push(roots.remove(i)); + } + } + } + + // Add them as children of the ongoing span + for child in to_reparent { + try_add_child_to_parent(roots, ongoing_id, child); + } + } + + roots.sort_by_key(|n| n.start); + sort_tree_children(roots); + } + + // Collect all span IDs that are ongoing (end: None) + fn collect_ongoing_ids(nodes: &[SpanNode]) -> Vec { + let mut ids = Vec::new(); + for node in nodes { + if node.end.is_none() { + ids.push(node.span_id); + } + ids.extend(collect_ongoing_ids(&node.children)); + } + ids + } + + // Calculate when thread was active (any span executing) + fn calculate_active_intervals( + completed: &[&CompletedSpan], + ongoing: &[(u64, Option, &'static str, Instant)], + current_time: Instant, + ) -> Vec<(Instant, Instant)> { + let mut intervals = Vec::new(); + + // Collect all time points + let mut events: Vec<(Instant, bool)> = Vec::new(); // (time, is_start) + + for span in completed { + events.push((span.start, true)); + events.push((span.end, false)); + } + + for &(_, _, _, start) in ongoing { + events.push((start, true)); + // Ongoing spans don't have an end yet + } + + events.sort_by_key(|(time, _)| *time); + + let mut depth = 0; + let mut interval_start = None; + + for (time, is_start) in events { + if is_start { + if depth == 0 { + interval_start = Some(time); + } + depth += 1; + } else { + depth -= 1; + if depth == 0 { + if let Some(start) = interval_start { + intervals.push((start, time)); + interval_start = None; + } + } + } + } + + // If we have ongoing spans, add current interval using current_time + if depth > 0 { + if let Some(start) = interval_start { + intervals.push((start, current_time)); + } + } + + intervals + } - fn handle_input(window: &mut Window, view: &mut ViewState, buffer: &RingBuffer, earliest: Instant, _width: usize, height: usize) { + fn handle_input( + window: &mut Window, + view: &mut ViewState, + frame_data: &FrameData, + width: usize, + _height: usize, + ) { // Pause/unpause if window.is_key_pressed(Key::Space, minifb::KeyRepeat::No) { view.paused = !view.paused; if view.paused { view.pause_guard = PAUSE.try_lock().ok(); + view.pause_timestamp = Some(Instant::now()); } else { view.pause_guard = None; + view.pause_timestamp = None; } } @@ -407,49 +793,22 @@ mod window { view.mouse_x = mx; view.mouse_y = my; - let icicle_height = height / 2; - let is_icicle = my < icicle_height as f32; - - // 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 + if scroll_y.abs() > 0.5 { + // Horizontal zoom let zoom_factor = if scroll_y > 0.0 { 1.2 } else { 1.0 / 1.2 }; - let old_scale = view.timeline_time_scale; + let old_scale = view.time_scale; let new_scale = old_scale * zoom_factor; - let mouse_time = view.timeline_time_offset + (mx as f64 / old_scale); + let mouse_time = view.time_offset + (mx as f64 / old_scale); - 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; - let mouse_time = view.icicle_time_offset + (mx as f64 / old_scale); - - 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; + view.time_scale = new_scale; + view.time_offset = mouse_time - (mx as f64 / new_scale); } 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); - } + view.scroll_y = (view.scroll_y - scroll_amount).max(0.0); } } } @@ -461,16 +820,31 @@ mod window { // Right click = pan if right_down && view.mouse_down { let dx = mx - view.last_mouse_x; - let delta = dx as f64 / view.icicle_time_scale; - view.icicle_time_offset -= delta; - view.timeline_time_offset -= delta; + let delta = dx as f64 / view.time_scale; + view.time_offset -= delta; } - // Left click = box selection for zoom + // Left click = box selection for zoom OR track toggle if left_down && !view.selecting && !view.mouse_down { - view.selecting = true; - view.selection_start_x = mx; - view.selection_end_x = mx; + // Check if clicking on a track header + if let Some(track_idx) = get_track_at_y(frame_data, my, view.scroll_y) { + let track = &frame_data.tracks[track_idx]; + let track_y = get_track_y(frame_data, track_idx, view.scroll_y); + + // Check if click is in header region + if my >= track_y && my < track_y + TRACK_HEADER_HEIGHT as f32 { + view.toggle_track(track.thread_id); + } else { + // Start box selection + view.selecting = true; + view.selection_start_x = mx; + view.selection_end_x = mx; + } + } else { + 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 { @@ -478,24 +852,21 @@ mod window { 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); + if (x2 - x1) > 5.0 { + let time1 = view.time_offset + (x1 as f64 / view.time_scale); + let time2 = view.time_offset + (x2 as f64 / view.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; + let new_scale = width as f64 / time_range; + view.time_scale = new_scale; + view.time_offset = time1; } view.selecting = false; } // Update hover detection - update_hover(view, buffer, earliest, mx, my, icicle_height); + update_hover(view, frame_data, mx, my); view.mouse_down = left_down || right_down; view.last_mouse_x = mx; @@ -505,81 +876,62 @@ mod window { view.hovered_span = None; } } + + fn get_track_at_y(frame_data: &FrameData, y: f32, scroll_y: f32) -> Option { + let mut current_y = -scroll_y; - fn update_hover(view: &mut ViewState, buffer: &RingBuffer, earliest: Instant, mx: f32, my: f32, icicle_height: usize) { + for (idx, track) in frame_data.tracks.iter().enumerate() { + let track_height = track.height() as f32; + if y >= current_y && y < current_y + track_height { + return Some(idx); + } + current_y += track_height; + } + + None + } + + fn get_track_y(frame_data: &FrameData, track_idx: usize, scroll_y: f32) -> f32 { + let mut y = -scroll_y; + + for (idx, track) in frame_data.tracks.iter().enumerate() { + if idx == track_idx { + return y; + } + y += track.height() as f32; + } + + y + } + + fn update_hover(view: &mut ViewState, frame_data: &FrameData, mx: f32, my: f32) { view.hovered_span = None; - let mouse_time = view.icicle_time_offset + (mx as f64 / view.icicle_time_scale); + let mouse_time = view.time_offset + (mx as f64 / view.time_scale); - if my < icicle_height as f32 { - // Icicle view hover - let row_height = 24; - let scroll_offset = view.icicle_scroll_y as i32; - let adjusted_y = my as i32 + scroll_offset; - let mouse_depth = (adjusted_y / row_height as i32).max(0) as usize; + if let Some(track_idx) = get_track_at_y(frame_data, my, view.scroll_y) { + let track = &frame_data.tracks[track_idx]; + let track_y = get_track_y(frame_data, track_idx, view.scroll_y); + let relative_y = my - track_y - TRACK_HEADER_HEIGHT as f32; - // Collect all spans and calculate their depths - let spans: Vec<_> = buffer.iter().collect(); - let depths = calculate_span_depths(&spans); - - // Find span under cursor that matches both time AND depth - for span in buffer.iter() { - if let Some(&span_depth) = depths.get(&span.span_id) { - if span_depth != mouse_depth { - continue; - } - - 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; - } - } - } - } else { - // Timeline view hover - let timeline_y = my - icicle_height as f32; - - // Collect all spans and group by thread - let spans: Vec<_> = buffer.iter().collect(); - let mut threads: HashMap> = HashMap::new(); - for span in &spans { - threads.entry(span.thread_id).or_default().push(*span); - } - - let mut thread_ids: Vec<_> = threads.keys().copied().collect(); - thread_ids.sort_unstable(); - let num_threads = thread_ids.len(); - - if num_threads > 0 { - let timeline_height = icicle_height; // Assuming timeline takes remaining half - let row_height = (timeline_height / num_threads.max(1)).max(20); - let mouse_thread_row = (timeline_y as usize) / row_height; - - if mouse_thread_row < thread_ids.len() { - let thread_id = thread_ids[mouse_thread_row]; - let thread_spans = &threads[&thread_id]; - - // Find span at this time in this thread - for span in thread_spans { - let start_time = (span.start - earliest).as_secs_f64(); - let end_time = (span.end - earliest).as_secs_f64(); + if relative_y >= 0.0 { + if track.expanded { + // Find span at depth + let depth = (relative_y / ROW_HEIGHT as f32) as usize; + find_span_at_depth(&track.root_spans, frame_data.earliest, mouse_time, depth, 0, view, track.thread_id); + } else { + // Collapsed - just show that thread is active + for &(start, end) in &track.active_intervals { + let start_time = (start - frame_data.earliest).as_secs_f64(); + let end_time = (end - frame_data.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, + name: "Active", + duration_us: (end_time - start_time) * 1_000_000.0, + thread_id: track.thread_id, start_time, + ongoing: false, }); break; } @@ -588,133 +940,71 @@ mod window { } } } - - fn calculate_span_depths(spans: &[&CompletedSpan]) -> HashMap { - let mut children: HashMap> = HashMap::new(); - let mut depths: HashMap = HashMap::new(); - let mut roots = Vec::new(); - - for span in spans { - if let Some(parent) = span.parent_id { - children.entry(parent).or_default().push(span); - } else { - roots.push(span.span_id); - depths.insert(span.span_id, 0); - } - } - - // Compute depths for all spans using BFS - let mut queue: Vec = roots.clone(); - while let Some(span_id) = queue.pop() { - let current_depth = depths[&span_id]; - if let Some(child_list) = children.get(&span_id) { - for child in child_list { - depths.insert(child.span_id, current_depth + 1); - queue.push(child.span_id); + + fn find_span_at_depth( + nodes: &[SpanNode], + earliest: Instant, + mouse_time: f64, + target_depth: usize, + current_depth: usize, + view: &mut ViewState, + thread_id: u64, + ) { + for node in nodes { + let start_time = (node.start - earliest).as_secs_f64(); + let end_time = node.end.map(|e| (e - earliest).as_secs_f64()) + .unwrap_or_else(|| (view.current_time() - earliest).as_secs_f64()); + + if mouse_time >= start_time && mouse_time <= end_time { + if current_depth == target_depth { + let duration_us = (end_time - start_time) * 1_000_000.0; + view.hovered_span = Some(HoveredSpan { + name: node.name, + duration_us, + thread_id, + start_time, + ongoing: node.end.is_none(), + }); + return; + } + + find_span_at_depth(&node.children, earliest, mouse_time, target_depth, current_depth + 1, view, thread_id); } } } - depths -} - fn render_frame( framebuffer: &mut [u32], width: usize, height: usize, - buffer: &RingBuffer, + frame_data: &FrameData, view: &ViewState, font: &fontdue::Font, ) { - let icicle_height = height / 2; - let timeline_height = height - icicle_height; - - let spans: Vec<_> = buffer.iter().collect(); - if spans.is_empty() { - return; - } - - let earliest = spans.iter().map(|s| s.start).min().unwrap(); - let _latest = spans.iter().map(|s| s.end).max().unwrap(); - - // Render icicle graph (top half) - render_icicle(framebuffer, width, icicle_height, &spans, earliest, view, font); - - // Draw separator line - for x in 0..width { - framebuffer[icicle_height * width + x] = 0x505050; - } - - // Render timeline (bottom half) - render_timeline( - framebuffer, - width, - height, - icicle_height, - timeline_height, - buffer, - earliest, - view, - font, - ); + let mut y = -(view.scroll_y as i32); - // Draw timestamp axis at the bottom of icicle view - draw_timestamp_axis(framebuffer, width, icicle_height - 20, view, earliest, font); + for track in &frame_data.tracks { + let track_height = track.height() as i32; + + // Only render if visible + if y + track_height > 0 && y < height as i32 { + render_track(framebuffer, width, height, y, track, frame_data, view, font); + } + + y += track_height; + } + + // Draw timestamp axis at the bottom + draw_timestamp_axis(framebuffer, width, height - 20, view, 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 - - for y in 0..height { - for x in x1..x2.min(width) { - let idx = y * width + x; - if idx < framebuffer.len() { - let bg = framebuffer[idx]; - let bg_r = ((bg >> 16) & 0xFF) as f32; - let bg_g = ((bg >> 8) & 0xFF) as f32; - let bg_b = (bg & 0xFF) as f32; - - 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_selection_box(framebuffer, width, height, view); } // 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_crosshair(framebuffer, width, height, view); } // Draw hover tooltip @@ -723,15 +1013,305 @@ mod window { } } + fn render_track( + framebuffer: &mut [u32], + width: usize, + height: usize, + y: i32, + track: &ThreadTrack, + frame_data: &FrameData, + view: &ViewState, + font: &fontdue::Font, + ) { + if y >= height as i32 { + return; + } + + // Draw header background + let header_y = y.max(0) as usize; + if header_y < height { + let header_height = TRACK_HEADER_HEIGHT.min(height - header_y); + fill_rect(framebuffer, width, 0, header_y, width, header_height, HEADER_BG); + + // Draw expand/collapse indicator + let indicator = if track.expanded { "▼" } else { "▶" }; + draw_text(framebuffer, width, 4, header_y + 4, indicator, font, 14.0, TEXT_COLOR); + + // Draw thread name + let default_name = format!("Thread {}", track.thread_id); + let name = track.name.as_ref() + .map(|s| s.as_str()) + .unwrap_or(&default_name); + draw_text(framebuffer, width, 24, header_y + 4, name, font, 14.0, TEXT_COLOR); + } + + let content_y = y + TRACK_HEADER_HEIGHT as i32; + + if track.expanded { + // Render full call stack + render_track_expanded(framebuffer, width, height, content_y, track, frame_data, view, font); + } else { + // Render collapsed (active time only) + render_track_collapsed(framebuffer, width, height, content_y, track, frame_data, view); + } + } + + fn render_track_expanded( + framebuffer: &mut [u32], + width: usize, + height: usize, + y: i32, + track: &ThreadTrack, + frame_data: &FrameData, + view: &ViewState, + font: &fontdue::Font, + ) { + // Render each root span and its children + for root in &track.root_spans { + render_span_node(framebuffer, width, height, y, root, frame_data, view, font, 0); + } + } + + fn render_span_node( + framebuffer: &mut [u32], + width: usize, + height: usize, + base_y: i32, + node: &SpanNode, + frame_data: &FrameData, + view: &ViewState, + font: &fontdue::Font, + depth: usize, + ) { + let y = base_y + (depth * ROW_HEIGHT) as i32; + + if y >= height as i32 || y + (ROW_HEIGHT as i32) < 0 { + return; + } + + let start_time = (node.start - frame_data.earliest).as_secs_f64(); + let end_time = node.end + .map(|e| (e - frame_data.earliest).as_secs_f64()) + .unwrap_or_else(|| (view.current_time() - frame_data.earliest).as_secs_f64()); + + let x1 = ((start_time - view.time_offset) * view.time_scale) as i32; + let x2 = ((end_time - view.time_offset) * view.time_scale) as i32; + + if x2 > 0 && x1 < width as i32 { + let color = get_color_for_name(node.name); + + // Clamp to visible area + let visible_x1 = x1.max(0) as usize; + let visible_x2 = (x2.min(width as i32) as usize).max(visible_x1); + let bar_width = visible_x2.saturating_sub(visible_x1); + + let inset_x = 2; + let inset_y = 1; + + let actual_width = if bar_width > inset_x * 2 { + bar_width.saturating_sub(inset_x * 2) + } else { + bar_width + }; + let actual_height = ROW_HEIGHT.saturating_sub(inset_y * 2); + + if actual_width > 0 && actual_height > 0 && y >= 0 { + fill_rect( + framebuffer, + width, + visible_x1.saturating_add(inset_x), + y as usize + inset_y, + actual_width, + actual_height, + color, + ); + + if actual_width > 40 { + draw_text( + framebuffer, + width, + visible_x1.saturating_add(inset_x + 4), + y as usize + inset_y + 2, + node.name, + font, + 12.0, + TEXT_COLOR, + ); + } + } + } + + // Render children + for child in &node.children { + render_span_node(framebuffer, width, height, base_y, child, frame_data, view, font, depth + 1); + } + } + + fn render_track_collapsed( + framebuffer: &mut [u32], + width: usize, + height: usize, + y: i32, + track: &ThreadTrack, + frame_data: &FrameData, + view: &ViewState, + ) { + if y >= height as i32 || y + (COLLAPSED_TRACK_HEIGHT as i32) < 0 { + return; + } + + let y = y.max(0) as usize; + + // Draw active intervals + for &(start, end) in &track.active_intervals { + let start_time = (start - frame_data.earliest).as_secs_f64(); + let end_time = (end - frame_data.earliest).as_secs_f64(); + + let x1 = ((start_time - view.time_offset) * view.time_scale) as i32; + let x2 = ((end_time - view.time_offset) * view.time_scale) as i32; + + if x2 > 0 && x1 < width as i32 { + // Clamp to visible area + let visible_x1 = x1.max(0) as usize; + let visible_x2 = (x2.min(width as i32) as usize).max(visible_x1); + let bar_width = visible_x2.saturating_sub(visible_x1); + + let inset_x = 2; + let inset_y = 1; + + let actual_width = if bar_width > inset_x * 2 { + bar_width.saturating_sub(inset_x * 2) + } else { + bar_width + }; + let actual_height = COLLAPSED_TRACK_HEIGHT.saturating_sub(inset_y * 2); + + if actual_width > 0 && actual_height > 0 { + fill_rect( + framebuffer, + width, + visible_x1.saturating_add(inset_x), + y + inset_y, + actual_width, + actual_height, + ACTIVE_COLOR, + ); + } + } + } + } + + fn draw_timestamp_axis( + framebuffer: &mut [u32], + width: usize, + y: usize, + view: &ViewState, + font: &fontdue::Font, + ) { + // Draw background bar + for x in 0..width { + let idx = y * width + x; + if idx < framebuffer.len() { + framebuffer[idx] = 0x1E1E1E; + } + } + + let visible_duration = width as f64 / view.time_scale; + let time_step = calculate_time_step(visible_duration); + + let start_time = view.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.time_offset) * view.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; + } + } + + 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 { + 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 draw_selection_box(framebuffer: &mut [u32], width: usize, height: usize, view: &ViewState) { + 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; + + for y in 0..height { + for x in x1..x2.min(width) { + let idx = y * width + x; + if idx < framebuffer.len() { + let bg = framebuffer[idx]; + framebuffer[idx] = blend_color(bg, selection_color, 0.3); + } + } + } + } + + fn draw_crosshair(framebuffer: &mut [u32], width: usize, height: usize, view: &ViewState) { + 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]); + } + } + } + } + fn blend_cursor(bg: u32) -> u32 { + blend_color(bg, 0xFFFFFF, 0.6) + } + + fn blend_color(bg: u32, fg: u32, alpha: f32) -> 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; + let fg_r = ((fg >> 16) & 0xFF) as f32; + let fg_g = ((fg >> 8) & 0xFF) as f32; + let fg_b = (fg & 0xFF) as f32; + + let r = (bg_r * (1.0 - alpha) + fg_r * alpha) as u32; + let g = (bg_g * (1.0 - alpha) + fg_g * alpha) as u32; + let b = (bg_b * (1.0 - alpha) + fg_b * alpha) as u32; (r << 16) | (g << 8) | b } @@ -744,36 +1324,30 @@ mod window { hover: &HoveredSpan, font: &fontdue::Font, ) { - let tooltip_w = 280; - let tooltip_h = 80; + let tooltip_w = 300; + let tooltip_h = if hover.ongoing { 100 } else { 80 }; let padding = 8; - let margin = 10; // Margin from screen edges - let offset = 20; // Offset from cursor + let margin = 10; + let offset = 20; - // Calculate initial position (offset from cursor) let mut tooltip_x = view.mouse_x as i32 + offset; let mut tooltip_y = view.mouse_y as i32 + offset; - // Check if tooltip would go off right edge if tooltip_x + tooltip_w as i32 + margin > width as i32 { - // Position to the left of cursor instead tooltip_x = view.mouse_x as i32 - tooltip_w as i32 - offset; } - // Check if tooltip would go off bottom edge if tooltip_y + tooltip_h as i32 + margin > height as i32 { - // Position above cursor instead tooltip_y = view.mouse_y as i32 - tooltip_h as i32 - offset; } - // Clamp to screen bounds with margin tooltip_x = tooltip_x.clamp(margin, (width as i32 - tooltip_w as i32 - margin).max(margin)); tooltip_y = tooltip_y.clamp(margin, (height as i32 - tooltip_h as i32 - margin).max(margin)); let tooltip_x = tooltip_x as usize; let tooltip_y = tooltip_y as usize; - // Draw tooltip background + // Draw background for dy in 0..tooltip_h { for dx in 0..tooltip_w { let x = tooltip_x + dx; @@ -781,11 +1355,10 @@ mod window { 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 + 0x808080 } else { - 0x1E1E1E // Background + 0x1E1E1E }; framebuffer[idx] = color; } @@ -804,285 +1377,10 @@ mod window { &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( - framebuffer: &mut [u32], - width: usize, - height: usize, - spans: &[&CompletedSpan], - earliest: Instant, - view: &ViewState, - font: &fontdue::Font, - ) { - // Calculate depths for all spans - let depths = calculate_span_depths(spans); - - // Group spans by depth level - let mut spans_by_depth: HashMap> = HashMap::new(); - for span in spans { - if let Some(&depth) = depths.get(&span.span_id) { - spans_by_depth.entry(depth).or_default().push(*span); - } - } - - // Find max depth - let max_depth = spans_by_depth.keys().max().copied().unwrap_or(0); - - // Render each depth level as a row (flame graph style) - let row_height = 24; - let scroll_offset = view.icicle_scroll_y as i32; - - for depth in 0..=max_depth { - let y = (depth * row_height) as i32 - scroll_offset; - - // Only render if in view - if y + row_height as i32 > 0 && y < height as i32 { - if let Some(depth_spans) = spans_by_depth.get(&depth) { - for span in depth_spans { - render_icicle_span_at_depth( - framebuffer, - width, - height, - span, - earliest, - y.max(0) as usize, - row_height, - view, - font, - ); - } - } - } - } - } - - fn render_icicle_span_at_depth( - framebuffer: &mut [u32], - width: usize, - height: usize, - span: &CompletedSpan, - earliest: Instant, - y: usize, - row_height: usize, - view: &ViewState, - font: &fontdue::Font, - ) { - if y + row_height > height { - return; - } - - let start_time = (span.start - earliest).as_secs_f64(); - let end_time = (span.end - earliest).as_secs_f64(); - - // Apply zoom and pan - let x1 = ((start_time - view.icicle_time_offset) * view.icicle_time_scale) as i32; - let x2 = ((end_time - view.icicle_time_offset) * view.icicle_time_scale) as i32; - - // 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; - - // Add inset padding - let inset_x = 2; - let inset_height = 1; - - let actual_width = if bar_width > inset_x * 2 { - bar_width.saturating_sub(inset_x * 2) - } else { bar_width }; - - let actual_height = row_height.saturating_sub(inset_height + 1); // +1 for bottom gap - - if actual_width > 0 && actual_height > 0 { - fill_rect( - framebuffer, - width, - (x1.max(0) as usize).saturating_add(inset_x), - y.saturating_add(inset_height), - actual_width, - actual_height, - color - ); - - // Render text if there's enough space - if actual_width > 40 { - draw_text( - framebuffer, - width, - (x1.max(0) as usize).saturating_add(inset_x + 4), - y.saturating_add(inset_height + 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 - ); - } - } - } - } - - fn render_timeline( - framebuffer: &mut [u32], - width: usize, - _height: usize, - y_offset: usize, - timeline_height: usize, - buffer: &RingBuffer, - earliest: Instant, - view: &ViewState, - font: &fontdue::Font, - ) { - // Collect all spans - let spans: Vec<_> = buffer.iter().collect(); - - // Group by thread and sort for stable ordering - let mut threads: HashMap> = HashMap::new(); - for span in &spans { - threads.entry(span.thread_id).or_default().push(*span); - } - - let mut thread_ids: Vec<_> = threads.keys().copied().collect(); - thread_ids.sort_unstable(); - let num_threads = thread_ids.len(); - - if num_threads == 0 { - return; - } - - let row_height = (timeline_height / num_threads.max(1)).max(20); - - for (i, thread_id) in thread_ids.iter().enumerate() { - let y = y_offset + i * row_height; - let thread_spans = &threads[thread_id]; - - // Draw thread label with name if available - let label = if let Some(name) = buffer.thread_names.get(thread_id) { - name.clone() - } else { - format!("Thread {}", thread_id) - }; - draw_text(framebuffer, width, 4, y + 4, &label, font, 12.0, TEXT_COLOR); - - for span in thread_spans { - let start_time = (span.start - earliest).as_secs_f64(); - let end_time = (span.end - earliest).as_secs_f64(); - - // Apply zoom and pan - let x1 = ((start_time - view.timeline_time_offset) * view.timeline_time_scale) as i32; - let x2 = ((end_time - view.timeline_time_offset) * view.timeline_time_scale) as i32; - - // 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; - - // Add inset padding: 2px horizontal, 1px vertical - let inset_x = 2; - let inset_y = 1; - - let actual_width = if bar_width > inset_x * 2 { - bar_width.saturating_sub(inset_x * 2) - } else { bar_width }; - let actual_height = row_height.saturating_sub(inset_y * 2 + 2); // Additional gap - - if actual_width > 0 && actual_height > 0 { - fill_rect( - framebuffer, - width, - (x1.max(0) as usize).saturating_add(inset_x), - y.saturating_add(inset_y), - actual_width, - actual_height, - color - ); - - // Render text if there's enough space - if actual_width > 60 { - draw_text( - framebuffer, - width, - (x1.max(0) as usize).saturating_add(inset_x + 4), - y + row_height / 2 - 6, - span.name, - font, - 12.0, - TEXT_COLOR - ); - } - } - } - } + if hover.ongoing { + draw_text(framebuffer, width, text_x, text_y + 74, + "Status: ONGOING", font, 12.0, 0xFF8800); } } @@ -1124,22 +1422,9 @@ mod window { if px < width { let idx = py * width + px; if idx < framebuffer.len() { - // Alpha blend let alpha_f = alpha as f32 / 255.0; let bg = framebuffer[idx]; - let bg_r = ((bg >> 16) & 0xFF) as f32; - let bg_g = ((bg >> 8) & 0xFF) as f32; - let bg_b = (bg & 0xFF) as f32; - - let fg_r = ((color >> 16) & 0xFF) as f32; - let fg_g = ((color >> 8) & 0xFF) as f32; - let fg_b = (color & 0xFF) as f32; - - let r = (bg_r * (1.0 - alpha_f) + fg_r * alpha_f) as u32; - let g = (bg_g * (1.0 - alpha_f) + fg_g * alpha_f) as u32; - let b = (bg_b * (1.0 - alpha_f) + fg_b * alpha_f) as u32; - - framebuffer[idx] = (r << 16) | (g << 8) | b; + framebuffer[idx] = blend_color(bg, color, alpha_f); } } }