Initial project import

This commit is contained in:
Agila Forge Importer committed 2026-07-13 15:20:32 +00:00
commit 23f2d29e32
11957 files changed
+1754577

No files matched your search

+346
View File
@@ -0,0 +1,346 @@
// Copyright (c) 2020-2024 Joe Conigliaro. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module main
import gg
import v2.profiler
// draw_header renders the top stats bar
fn draw_header(mut app App) {
ctx := app.gg
w := f32(ctx.width)
// Background
ctx.draw_rect_filled(0, 0, w, f32(app.header_height), header_bg)
// Get stats
stats := app.cached_stats
// Format and draw stats
heap_str := 'HEAP: ${profiler.format_bytes(stats.live_bytes)}'
peak_str := 'Peak: ${profiler.format_bytes(stats.peak_bytes)}'
allocs_str := '${stats.total_allocs} allocs'
frees_str := '${stats.total_frees} frees'
leaks_str := '${stats.leak_count} leaks'
// Draw stats with spacing
mut x := f32(20)
y := f32(22)
ctx.draw_text(int(x), int(y), heap_str, color: text_color, size: 20)
x += 180
ctx.draw_text(int(x), int(y), peak_str, color: text_color, size: 20)
x += 160
ctx.draw_text(int(x), int(y), allocs_str, color: new_alloc_color, size: 20)
x += 140
ctx.draw_text(int(x), int(y), frees_str, color: freed_color, size: 20)
x += 120
// Show leaks in red if there are any
leak_clr := if stats.leak_count > 0 { leak_color } else { text_color }
ctx.draw_text(int(x), int(y), leaks_str, color: leak_clr, size: 20)
// Mode and frame counter on right
mode_str := if app.demo_mode {
'DEMO'
} else if app.live_mode {
'LIVE'
} else {
'LOCAL'
}
ctx.draw_text(int(w) - 250, int(y), mode_str, color: gg.Color{100, 255, 100, 255}, size: 20)
frame_str := 'Frame: ${stats.frame_count}'
ctx.draw_text(int(w) - 150, int(y), frame_str, color: text_color, size: 20)
// Separator line
ctx.draw_line(0, f32(app.header_height), w, f32(app.header_height), grid_color)
}
// draw_histogram renders the allocation histogram
fn draw_histogram(mut app App) {
ctx := app.gg
w := f32(ctx.width)
// Histogram area
hist_y := f32(app.header_height)
hist_h := f32(app.histogram_height)
center_y := hist_y + hist_h / 2
// Background
ctx.draw_rect_filled(0, hist_y, w, hist_h, bg_color)
// Center line
ctx.draw_line(0, center_y, w, center_y, grid_color)
// Get frame data (always use cached data - it's populated by update_cache from snapshot or demo)
frames := app.cached_frames
if frames.len == 0 {
ctx.draw_text(int(w / 2) - 80, int(center_y) - 10, 'No frame data',
color: text_color
size: 18
)
return
}
// Calculate max values for scaling
mut max_new := u64(1)
mut max_freed := u64(1)
for frame in frames {
if frame.new_bytes > max_new {
max_new = frame.new_bytes
}
if frame.freed_bytes > max_freed {
max_freed = frame.freed_bytes
}
}
// Bar dimensions
margin := f32(40)
bar_area_width := w - margin * 2
bar_width := bar_area_width / f32(frames.len)
if bar_width < 1 {
return
}
bar_gap := bar_width * 0.1
actual_bar_w := bar_width - bar_gap
max_bar_height := (hist_h / 2) - 10
// Draw bars for each frame
for i, frame in frames {
x := margin + f32(i) * bar_width
// New allocations (cyan, above center)
if frame.new_bytes > 0 {
new_height := f32(frame.new_bytes) / f32(max_new) * max_bar_height
bar_color := if i == app.selected_frame { selected_color } else { new_alloc_color }
ctx.draw_rect_filled(x, center_y - new_height, actual_bar_w, new_height, bar_color)
}
// Freed allocations (purple, below center)
if frame.freed_bytes > 0 {
freed_height := f32(frame.freed_bytes) / f32(max_freed) * max_bar_height
bar_color := if i == app.selected_frame {
gg.Color{150, 130, 255, 200}
} else {
freed_color
}
ctx.draw_rect_filled(x, center_y, actual_bar_w, freed_height, bar_color)
}
// Hover highlight
if i == app.hover_frame && i != app.selected_frame {
ctx.draw_rect_empty(x - 1, hist_y + 5, actual_bar_w + 2, hist_h - 10, timeline_cursor)
}
}
// Legend
ctx.draw_rect_filled(10, hist_y + 10, 12, 12, new_alloc_color)
ctx.draw_text(26, int(hist_y) + 8, 'New', color: text_color, size: 14)
ctx.draw_rect_filled(70, hist_y + 10, 12, 12, freed_color)
ctx.draw_text(86, int(hist_y) + 8, 'Freed', color: text_color, size: 14)
// Separator line
ctx.draw_line(0, hist_y + hist_h, w, hist_y + hist_h, grid_color)
}
// draw_timeline renders the frame timeline with scrubber
fn draw_timeline(mut app App) {
ctx := app.gg
w := f32(ctx.width)
// Timeline area
timeline_y := f32(app.header_height + app.histogram_height)
timeline_h := f32(app.timeline_height)
// Background
ctx.draw_rect_filled(0, timeline_y, w, timeline_h, header_bg)
frames := app.cached_frames
if frames.len == 0 {
return
}
// Draw frame ticks
margin := f32(40)
tick_area_width := w - margin * 2
tick_spacing := tick_area_width / f32(frames.len)
// Draw tick marks
for i := 0; i < frames.len; i++ {
x := margin + f32(i) * tick_spacing
tick_height := if i % 10 == 0 { f32(15) } else { f32(8) }
ctx.draw_line(x, timeline_y + 20, x, timeline_y + 20 + tick_height, grid_color)
// Frame number labels every 10 frames
if i % 10 == 0 && tick_spacing > 3 {
ctx.draw_text(int(x) - 10, int(timeline_y) + 40, '${i}', color: text_color, size: 12)
}
}
// Draw selected frame cursor
if app.selected_frame >= 0 && app.selected_frame < frames.len {
cursor_x := margin + f32(app.selected_frame) * tick_spacing
// Triangle cursor
draw_triangle_filled(ctx, cursor_x, timeline_y + 15, cursor_x - 8, timeline_y + 5,
cursor_x + 8, timeline_y + 5, timeline_cursor)
// Vertical line
ctx.draw_line(cursor_x, timeline_y + 15, cursor_x, timeline_y + timeline_h - 5,
timeline_cursor)
// Frame info
frame := frames[app.selected_frame]
info := 'Frame ${app.selected_frame}: +${profiler.format_bytes(frame.new_bytes)} / -${profiler.format_bytes(frame.freed_bytes)}'
ctx.draw_text(int(w / 2) - 100, int(timeline_y) + 55, info, color: timeline_cursor, size: 16)
}
// Separator line
ctx.draw_line(0, timeline_y + timeline_h, w, timeline_y + timeline_h, grid_color)
}
// draw_controls renders the filter controls
fn draw_controls(mut app App) {
ctx := app.gg
w := f32(ctx.width)
// Controls area
ctrl_y := f32(app.header_height + app.histogram_height + app.timeline_height)
ctrl_h := f32(app.controls_height)
// Background
ctx.draw_rect_filled(0, ctrl_y, w, ctrl_h, bg_color)
// Filter buttons
mut x := f32(20)
y := ctrl_y + 10
// Filter label
ctx.draw_text(int(x), int(y), 'Filter:', color: text_color, size: 16)
x += 60
// Filter mode buttons
filters := ['All', 'Leaks', 'Large', 'Frame']
modes := [FilterMode.all, FilterMode.new_not_freed, FilterMode.large, FilterMode.current_frame]
for i, label in filters {
btn_w := f32(60)
btn_h := f32(24)
// Highlight active filter
btn_color := if app.filter_mode == modes[i] { selected_color } else { header_bg }
ctx.draw_rect_filled(x, y - 2, btn_w, btn_h, btn_color)
ctx.draw_rect_empty(x, y - 2, btn_w, btn_h, grid_color)
ctx.draw_text(int(x) + 8, int(y), label, color: text_color, size: 14)
x += btn_w + 10
}
// Separator line
ctx.draw_line(0, ctrl_y + ctrl_h, w, ctrl_y + ctrl_h, grid_color)
}
// draw_details renders the selection details panel
fn draw_details(mut app App) {
ctx := app.gg
w := f32(ctx.width)
h := f32(ctx.height)
// Details area
details_y := f32(app.header_height + app.histogram_height + app.timeline_height +
app.controls_height)
details_h := h - details_y
// Background
ctx.draw_rect_filled(0, details_y, w, details_h, header_bg)
// Show selected allocation info or instructions
if app.selected_alloc >= 0 {
allocs := app.cached_allocs
if app.selected_alloc < allocs.len {
alloc := allocs[app.selected_alloc]
// Format allocation info
ptr_str := 'Address: 0x${voidptr(alloc.ptr):p}'
size_str := 'Size: ${profiler.format_bytes(u64(alloc.size))} (${alloc.size} bytes)'
frame_str := 'Allocated: frame ${alloc.frame}'
status_str := if alloc.freed {
'Status: FREED (frame ${alloc.free_frame})'
} else {
'Status: LIVE (potential leak)'
}
status_color := if alloc.freed { freed_color } else { leak_color }
mut y := details_y + 15
ctx.draw_text(20, int(y), ptr_str, color: text_color, size: 16)
ctx.draw_text(250, int(y), size_str, color: text_color, size: 16)
ctx.draw_text(500, int(y), frame_str, color: text_color, size: 16)
ctx.draw_text(700, int(y), status_str, color: status_color, size: 16)
// Source location if available
if alloc.file.len > 0 {
y += 25
loc_str := 'Source: ${alloc.file}:${alloc.line}'
ctx.draw_text(20, int(y), loc_str, color: new_alloc_color, size: 16)
}
}
} else {
// Instructions
ctx.draw_text(20, int(details_y) + 20,
'Click on a histogram bar to select a frame, or use keyboard arrows to navigate',
color: text_color
size: 16
)
}
}
// draw_frame is the main frame callback
pub fn draw_frame(mut app App) {
if app.gg == unsafe { nil } {
return
}
app.gg.begin()
// Update data
if app.demo_mode {
update_demo_data(mut app)
} else {
app.update_cache()
}
// Draw all sections
draw_header(mut app)
draw_histogram(mut app)
draw_timeline(mut app)
draw_controls(mut app)
draw_details(mut app)
// Draw status message if in live mode with no data
if app.live_mode && app.cached_frames.len == 0 {
ctx := app.gg
w := f32(ctx.width)
h := f32(ctx.height)
ctx.draw_text(int(w / 2) - 200, int(h / 2), app.status_msg,
color: text_color
size: 20
)
}
app.gg.end()
}
// Helper: draw a filled triangle
fn draw_triangle_filled(ctx &gg.Context, x1 f32, y1 f32, x2 f32, y2 f32, x3 f32, y3 f32, c gg.Color) {
ctx.draw_convex_poly([x1, y1, x2, y2, x3, y3], c)
}
+233
View File
@@ -0,0 +1,233 @@
// Copyright (c) 2020-2024 Joe Conigliaro. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module main
import gg
import os
import v2.profiler
// handle_event processes user input events
pub fn handle_event(e &gg.Event, mut app App) {
match e.typ {
.mouse_down {
handle_mouse_click(e, mut app)
}
.mouse_move {
handle_mouse_move(e, mut app)
}
.key_down {
handle_key_down(e, mut app)
}
else {}
}
}
// handle_mouse_click processes mouse click events
fn handle_mouse_click(e &gg.Event, mut app App) {
x := e.mouse_x
y := e.mouse_y
// Check which region was clicked
hist_y := f32(app.header_height)
hist_h := f32(app.histogram_height)
timeline_y := hist_y + hist_h
timeline_h := f32(app.timeline_height)
ctrl_y := timeline_y + timeline_h
ctrl_h := f32(app.controls_height)
// Click in histogram area
if y >= hist_y && y < hist_y + hist_h {
handle_histogram_click(x, y, mut app)
return
}
// Click in timeline area
if y >= timeline_y && y < timeline_y + timeline_h {
handle_timeline_click(x, y, mut app)
return
}
// Click in controls area
if y >= ctrl_y && y < ctrl_y + ctrl_h {
handle_controls_click(x, y, mut app)
return
}
}
// handle_histogram_click selects a frame from the histogram
fn handle_histogram_click(x f32, y f32, mut app App) {
frames := app.cached_frames
if frames.len == 0 {
return
}
w := f32(app.gg.width)
margin := f32(40)
bar_area_width := w - margin * 2
// Calculate frame index
relative_x := x - margin
if relative_x < 0 || relative_x > bar_area_width {
return
}
bar_width := bar_area_width / f32(frames.len)
frame_idx := int(relative_x / bar_width)
if frame_idx >= 0 && frame_idx < frames.len {
app.selected_frame = frame_idx
// Select first allocation in this frame
allocs := app.cached_allocs
for i, alloc in allocs {
if alloc.frame == u64(frame_idx) {
app.selected_alloc = i
break
}
}
}
}
// handle_timeline_click scrubs the timeline
fn handle_timeline_click(x f32, y f32, mut app App) {
// Same logic as histogram click for frame selection
handle_histogram_click(x, y, mut app)
}
// handle_controls_click handles filter button clicks
fn handle_controls_click(x f32, y f32, mut app App) {
// Filter buttons start at x=80
btn_start := f32(80)
btn_w := f32(60)
btn_gap := f32(10)
relative_x := x - btn_start
if relative_x < 0 {
return
}
btn_idx := int(relative_x / (btn_w + btn_gap))
modes := [FilterMode.all, FilterMode.new_not_freed, FilterMode.large, FilterMode.current_frame]
if btn_idx >= 0 && btn_idx < modes.len {
app.filter_mode = modes[btn_idx]
}
}
// handle_mouse_move updates hover state
fn handle_mouse_move(e &gg.Event, mut app App) {
x := e.mouse_x
y := e.mouse_y
hist_y := f32(app.header_height)
hist_h := f32(app.histogram_height)
// Check if hovering over histogram
if y >= hist_y && y < hist_y + hist_h {
frames := app.cached_frames
if frames.len > 0 {
w := f32(app.gg.width)
margin := f32(40)
bar_area_width := w - margin * 2
relative_x := x - margin
if relative_x >= 0 && relative_x <= bar_area_width {
bar_width := bar_area_width / f32(frames.len)
app.hover_frame = int(relative_x / bar_width)
if app.hover_frame >= frames.len {
app.hover_frame = -1
}
} else {
app.hover_frame = -1
}
}
} else {
app.hover_frame = -1
}
}
// handle_key_down processes keyboard input
fn handle_key_down(e &gg.Event, mut app App) {
match e.key_code {
.left {
// Previous frame
if app.selected_frame > 0 {
app.selected_frame--
}
}
.right {
// Next frame
frames := app.cached_frames
if app.selected_frame < frames.len - 1 {
app.selected_frame++
}
}
.up {
// Previous allocation
if app.selected_alloc > 0 {
app.selected_alloc--
}
}
.down {
// Next allocation
allocs := app.cached_allocs
if app.selected_alloc < allocs.len - 1 {
app.selected_alloc++
}
}
.enter {
// Open source file in editor
open_in_editor(app)
}
.f {
// Cycle filter modes
app.filter_mode = match app.filter_mode {
.all { .new_not_freed }
.new_not_freed { .large }
.large { .current_frame }
.current_frame { .all }
}
}
.r {
// Reset profiler data
if !app.demo_mode {
profiler.reset()
}
}
.escape {
// Clear selection
app.selected_frame = -1
app.selected_alloc = -1
}
.space {
// Toggle demo mode
app.demo_mode = !app.demo_mode
if app.demo_mode {
// Initialize demo data
init_demo_data(mut app)
}
}
else {}
}
}
// open_in_editor opens the selected allocation's source file in VS Code
fn open_in_editor(app &App) {
if app.selected_alloc < 0 {
return
}
allocs := app.cached_allocs
if app.selected_alloc >= allocs.len {
return
}
alloc := allocs[app.selected_alloc]
if alloc.file.len == 0 {
return
}
// Try to open in VS Code with line number
cmd := 'code -g "${alloc.file}:${alloc.line}"'
os.execute(cmd)
}
+154
View File
@@ -0,0 +1,154 @@
// Copyright (c) 2020-2024 Joe Conigliaro. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module main
import gg
import os
import v2.profiler
fn main() {
// Initialize profiler system
profiler.profiler_init()
// Create application state (on stack, like the working example)
mut app := App{}
// Check command line args for mode
args := os.args
if '--demo' in args {
app.demo_mode = true
app.live_mode = false
init_demo_data(mut app)
} else {
// Default to live mode - read from snapshot file
app.demo_mode = false
app.live_mode = true
}
// Create gg context
app.gg = gg.new_context(
width: window_width
height: window_height
create_window: true
window_title: 'V2 Allocation Profiler'
bg_color: bg_color
frame_fn: frame
event_fn: event_handler
user_data: &app
)
// Run the application
app.gg.run()
}
// frame is the frame callback for gg
fn frame(mut app App) {
draw_frame(mut app)
}
// event_handler handles gg events
fn event_handler(e &gg.Event, mut app App) {
handle_event(e, mut app)
}
// init_demo_data populates the profiler with simulated data for demonstration
fn init_demo_data(mut app App) {
app.demo_mode = true
app.demo_frame = 0
// Create simulated frame and allocation data
app.cached_frames = []profiler.FrameData{cap: 100}
app.cached_allocs = []profiler.AllocRecord{cap: 500}
// Simulate 100 frames of allocation activity
mut total_live := u64(0)
mut peak := u64(0)
mut alloc_idx := 0
mut total_allocs := u64(0)
mut total_frees := u64(0)
for frame_num := 0; frame_num < 100; frame_num++ {
mut frame_data := profiler.FrameData{
frame_num: u64(frame_num)
}
// Simulate some allocations each frame
// More allocations early, fewer later (typical startup pattern)
num_allocs := if frame_num < 20 { 10 - frame_num / 4 } else { 2 + (frame_num % 3) }
for _ in 0 .. num_allocs {
// Random-ish allocation sizes
size := 64 + ((frame_num * 17 + alloc_idx * 31) % 4096)
app.cached_allocs << profiler.AllocRecord{
ptr: unsafe { voidptr(u64(0x1000) + u64(alloc_idx) * 0x100) }
size: size
frame: u64(frame_num)
file: ['main.v', 'parser.v', 'scanner.v', 'types.v', 'gen.v'][alloc_idx % 5]
line: 100 + (alloc_idx % 500)
timestamp: i64(frame_num) * 16_666_667 + i64(alloc_idx) * 1000
freed: false
}
frame_data.new_allocs << alloc_idx
frame_data.new_bytes += u64(size)
total_live += u64(size)
total_allocs++
alloc_idx++
}
// Simulate some frees (but not all - create some "leaks")
// Free some allocations from previous frames
if frame_num > 5 {
num_frees := num_allocs - 1 // Always free slightly fewer than we allocate
mut freed_count := 0
for i := 0; i < app.cached_allocs.len && freed_count < num_frees; i++ {
if !app.cached_allocs[i].freed && app.cached_allocs[i].frame < u64(frame_num - 3) {
// 80% chance to free
if (i * 7 + frame_num * 13) % 10 < 8 {
app.cached_allocs[i].freed = true
app.cached_allocs[i].free_frame = u64(frame_num)
frame_data.freed_idxs << i
frame_data.freed_bytes += u64(app.cached_allocs[i].size)
total_live -= u64(app.cached_allocs[i].size)
total_frees++
freed_count++
}
}
}
}
frame_data.live_bytes = total_live
if total_live > peak {
peak = total_live
}
app.cached_frames << frame_data
}
// Count leaks
mut leak_count := 0
for alloc in app.cached_allocs {
if !alloc.freed {
leak_count++
}
}
// Update stats
app.cached_stats = profiler.Statistics{
live_bytes: total_live
peak_bytes: peak
total_allocs: total_allocs
total_frees: total_frees
leak_count: leak_count
frame_count: 100
}
}
// update_demo_data simulates ongoing activity in demo mode
fn update_demo_data(mut app App) {
// In demo mode, we just use the static simulated data
// For a more dynamic demo, we could add new frames here
app.demo_frame++
}
+176
View File
@@ -0,0 +1,176 @@
// Copyright (c) 2020-2024 Joe Conigliaro. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module main
import gg
import v2.profiler
// Filter modes for allocation view
pub enum FilterMode {
all // Show all allocations
new_not_freed // Show allocations that haven't been freed (potential leaks)
large // Show allocations larger than threshold
current_frame // Show allocations from selected frame only
}
// App holds the GUI application state
pub struct App {
pub mut:
gg &gg.Context = unsafe { nil }
// View state
selected_frame int = -1 // -1 means no selection
selected_alloc int = -1 // Index of selected allocation
filter_mode FilterMode = .all
large_threshold int = 1024 // Threshold for "large" filter (bytes)
search_text string
// Scroll/pan state
timeline_scroll int // Horizontal scroll for timeline
histogram_scroll int // Scroll for histogram if needed
// Cached data (updated each frame)
cached_frames []profiler.FrameData
cached_allocs []profiler.AllocRecord
cached_stats profiler.Statistics
// Layout dimensions
header_height int = 60
histogram_height int = 200
timeline_height int = 80
controls_height int = 40
details_height int = 60
// Hover state
hover_frame int = -1
hover_alloc int = -1
// Mode
demo_mode bool // Use simulated data for demo (false = live mode)
demo_frame int // Current demo frame counter
live_mode bool = true // Read from profiler snapshot file
// Status
status_msg string = 'Waiting for profiler data...'
}
// Colors for the profiler visualization
pub const bg_color = gg.Color{30, 30, 30, 255} // Dark gray background
pub const new_alloc_color = gg.Color{0, 200, 255, 200} // Cyan for new allocations
pub const freed_color = gg.Color{180, 100, 255, 200} // Purple for freed allocations
pub const leak_color = gg.Color{255, 100, 100, 255} // Red for leaks/live
pub const timeline_cursor = gg.Color{255, 255, 0, 255} // Yellow for timeline cursor
pub const text_color = gg.Color{220, 220, 220, 255} // Light gray text
pub const header_bg = gg.Color{40, 40, 45, 255} // Slightly lighter header
pub const grid_color = gg.Color{60, 60, 60, 255} // Grid lines
pub const selected_color = gg.Color{100, 150, 255, 255} // Selection highlight
// Window dimensions
pub const window_width = 1200
pub const window_height = 700
// init_app creates a new App instance (unused, kept for reference)
fn init_app_heap() &App {
return &App{}
}
// update_cache refreshes cached profiler data
pub fn (mut app App) update_cache() {
if app.demo_mode {
return
}
if app.live_mode {
// Read from snapshot file
if snap := profiler.read_snapshot() {
app.cached_frames = snap.frames
app.cached_allocs = snap.allocs
app.cached_stats = profiler.Statistics{
live_bytes: snap.live_bytes
peak_bytes: snap.peak_bytes
total_allocs: snap.total_allocs
total_frees: snap.total_frees
leak_count: count_leaks(snap.allocs)
frame_count: snap.frame_count
}
app.status_msg = 'Live: Frame ${snap.frame_count}'
} else {
app.status_msg = 'Waiting for profiler data at ${profiler.profiler_data_path}...'
}
return
}
// In-process mode (same process)
app.cached_frames = profiler.get_frames()
app.cached_allocs = profiler.get_allocs()
app.cached_stats = profiler.get_statistics()
}
fn count_leaks(allocs []profiler.AllocRecord) int {
mut count := 0
for alloc in allocs {
if !alloc.freed {
count++
}
}
return count
}
// get_filtered_allocs returns allocations matching current filter
pub fn (app &App) get_filtered_allocs() []profiler.AllocRecord {
mut result := []profiler.AllocRecord{}
allocs := app.cached_allocs
for alloc in allocs {
match app.filter_mode {
.all {
result << alloc
}
.new_not_freed {
if !alloc.freed {
result << alloc
}
}
.large {
if alloc.size >= app.large_threshold {
result << alloc
}
}
.current_frame {
if app.selected_frame >= 0 && alloc.frame == u64(app.selected_frame) {
result << alloc
}
}
}
}
// Apply search filter if text is present
if app.search_text.len > 0 {
mut filtered := []profiler.AllocRecord{}
for alloc in result {
if alloc.file.contains(app.search_text) {
filtered << alloc
}
}
return filtered
}
return result
}
// get_frame_at_x returns the frame index at the given x coordinate in timeline
pub fn (app &App) get_frame_at_x(x f32, timeline_x f32, timeline_width f32) int {
frames := app.cached_frames
if frames.len == 0 {
return -1
}
// Calculate which frame this x position corresponds to
relative_x := x - timeline_x
if relative_x < 0 || relative_x > timeline_width {
return -1
}
frame_width := timeline_width / f32(frames.len)
frame_idx := int(relative_x / frame_width)
if frame_idx >= 0 && frame_idx < frames.len {
return frame_idx
}
return -1
}