use astrelis_core::logging;
use astrelis_core::profiling::{ProfilingBackend, init_profiling, new_frame};
use astrelis_render::{GraphicsContext, RenderWindow, RenderWindowBuilder, wgpu};
use astrelis_ui::{AlignItems, FlexDirection, FlexWrap, JustifyContent, UiSystem, WidgetId};
use astrelis_winit::{
WindowId,
app::run_app,
event::{Event, HandleStatus, Key, NamedKey},
window::{Window, WindowDescriptor, WinitPhysicalSize},
};
use std::collections::HashMap;
use std::time::{Duration, Instant};
struct App {
windows: HashMap<WindowId, RenderWindow>,
ui: UiSystem,
last_update: Instant,
frame_count: u64,
last_metrics_print: Instant,
inspector_enabled: bool,
}
fn main() {
logging::init();
init_profiling(ProfilingBackend::PuffinHttp);
run_app(|ctx| {
let graphics_ctx =
GraphicsContext::new_owned_sync().expect("Failed to create graphics context");
let mut windows = HashMap::new();
let scale = Window::platform_dpi() as f32;
let window = ctx
.create_window(WindowDescriptor {
title: "Astrelis UI - Dashboard Performance".to_string(),
size: Some(WinitPhysicalSize::new(1280.0 * scale, 800.0 * scale)),
..Default::default()
})
.expect("Failed to create window");
let renderable_window = RenderWindowBuilder::new()
.color_format(wgpu::TextureFormat::Bgra8UnormSrgb)
.with_depth_default()
.build(window, graphics_ctx.clone())
.expect("Failed to create render window");
let window_id = renderable_window.id();
let viewport = renderable_window.viewport();
let mut ui = UiSystem::from_window(graphics_ctx.clone(), &renderable_window);
ui.set_viewport(viewport);
windows.insert(window_id, renderable_window);
build_dashboard(&mut ui);
println!("\n═══════════════════════════════════════════════════════");
println!(" 📊 UI DASHBOARD - Incremental Update Performance");
println!("═══════════════════════════════════════════════════════");
println!(" PURPOSE:");
println!(" Tests .update_text() API performance (dirty flags)");
println!(" 100 values update 60x/sec WITHOUT rebuilding UI tree");
println!("\n COMPARE WITH:");
println!(" ui_stress_test - Tests full UI rebuild performance");
println!("\n CONTROLS:");
println!(" [F12] Toggle UI Inspector");
println!("═══════════════════════════════════════════════════════\n");
Box::new(App {
windows,
ui,
last_update: Instant::now(),
frame_count: 0,
last_metrics_print: Instant::now(),
inspector_enabled: false,
})
});
}
fn build_dashboard(ui: &mut UiSystem) {
let theme = ui.theme().clone();
let colors = &theme.colors;
let bg = colors.background;
let surface = colors.surface;
let text_primary = colors.text_primary;
let text_secondary = colors.text_secondary;
let success = colors.success;
ui.build(|root| {
root.container()
.width(1280.0)
.height(800.0)
.background_color(bg)
.padding(20.0)
.flex_direction(FlexDirection::Column)
.child(|col| {
col.row()
.height(60.0)
.justify_content(JustifyContent::SpaceBetween)
.align_items(AlignItems::Center)
.child(|header| {
header
.text("System Telemetry")
.size(24.0)
.bold()
.color(text_primary)
.build();
header
.text("Status: ONLINE")
.size(16.0)
.color(success)
.build()
})
.build();
col.container()
.flex_direction(FlexDirection::Row)
.flex_wrap(FlexWrap::Wrap)
.gap(10.0)
.children(|grid| {
let mut ids = Vec::new();
for i in 0..100 {
let card_id = grid
.container()
.width(230.0)
.height(100.0)
.background_color(surface)
.border_radius(8.0)
.padding(15.0)
.child(|card| {
card.column()
.gap(5.0)
.child(|content| {
content
.text(format!("Sensor #{}", i))
.size(14.0)
.color(text_secondary)
.build();
content
.text("0.000")
.size(28.0)
.bold()
.color(text_primary)
.id(WidgetId::new(&format!("value_{}", i)))
.build()
})
.build()
})
.build();
ids.push(card_id);
}
ids
})
.build()
})
.build();
});
}
impl astrelis_winit::app::App for App {
fn update(
&mut self,
_ctx: &mut astrelis_winit::app::AppCtx,
_time: &astrelis_winit::FrameTime,
) {
new_frame();
let now = Instant::now();
let _dt = now.duration_since(self.last_update).as_secs_f32();
self.last_update = now;
self.frame_count += 1;
for i in 0..100 {
let value = (now.elapsed().as_secs_f32() * (i as f32 * 0.1)).sin() * 100.0;
let id = WidgetId::new(&format!("value_{}", i));
self.ui.update_text(id, format!("{:.3}", value));
}
if now.duration_since(self.last_metrics_print) > Duration::from_secs(1) {
let metrics = self.ui.core().tree().last_metrics();
if let Some(m) = metrics {
println!(
"FPS: {:.1} | Layout: {:.2}ms | Text Dirty: {} | Paint Dirty: {}",
self.frame_count as f32,
m.layout_time.as_secs_f64() * 1000.0,
m.nodes_text_dirty,
m.nodes_paint_dirty
);
self.ui.log_text_cache_stats();
}
self.last_metrics_print = now;
self.frame_count = 0;
}
}
fn render(
&mut self,
_ctx: &mut astrelis_winit::app::AppCtx,
window_id: WindowId,
events: &mut astrelis_winit::event::EventBatch,
) {
let Some(window) = self.windows.get_mut(&window_id) else {
return;
};
events.dispatch(|event| {
if let Event::WindowResized(size) = event {
window.resized(*size);
self.ui.set_viewport(window.viewport());
HandleStatus::consumed()
} else {
HandleStatus::ignored()
}
});
events.dispatch(|event| {
if let Event::KeyInput(key) = event
&& key.state == astrelis_winit::event::ElementState::Pressed
&& matches!(&key.logical_key, Key::Named(NamedKey::F12))
{
self.inspector_enabled = !self.inspector_enabled;
println!(
"Inspector: {}",
if self.inspector_enabled { "ON" } else { "OFF" }
);
return HandleStatus::consumed();
}
HandleStatus::ignored()
});
let bg = self.ui.theme().colors.background;
let Some(frame) = window.begin_frame() else {
return; };
{
let mut pass = frame
.render_pass()
.clear_color(bg)
.with_window_depth()
.clear_depth(0.0)
.label("UI")
.build();
self.ui.render(pass.wgpu_pass());
}
}
}