#[cfg(feature = "std")]
use embedded_graphics_simulator::{
BinaryColorTheme, OutputSettingsBuilder, SimulatorDisplay, SimulatorEvent, Window,
};
#[cfg(all(feature = "std", feature = "capture"))]
use super::capture::{self, capture_screenshot, GifCapture};
use embedded_graphics::{pixelcolor::Rgb565, prelude::Size, primitives::Rectangle};
use embedded_charts::prelude::*;
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)] pub enum WindowTheme {
Default,
Dark,
OledBlue,
OledWhite,
Custom { pixel_spacing: u32, scale: u32 },
}
#[allow(dead_code)] impl WindowTheme {
#[cfg(feature = "std")]
fn to_binary_color_theme(self) -> BinaryColorTheme {
match self {
WindowTheme::Default => BinaryColorTheme::Default,
WindowTheme::Dark => BinaryColorTheme::OledBlue,
WindowTheme::OledBlue => BinaryColorTheme::OledBlue,
WindowTheme::OledWhite => BinaryColorTheme::OledWhite,
WindowTheme::Custom { .. } => BinaryColorTheme::Default,
}
}
pub fn pixel_spacing(self) -> u32 {
match self {
WindowTheme::Custom { pixel_spacing, .. } => pixel_spacing,
_ => 0, }
}
pub fn scale(self) -> u32 {
match self {
WindowTheme::Custom { scale, .. } => scale,
WindowTheme::Default | WindowTheme::Dark => 1,
WindowTheme::OledBlue | WindowTheme::OledWhite => 2,
}
}
}
#[derive(Debug, Clone)]
#[allow(dead_code)] pub struct WindowConfig {
pub title: &'static str,
pub theme: WindowTheme,
pub target_fps: u32,
pub auto_close: bool,
pub background_color: Rgb565,
pub size: Size,
#[cfg(feature = "capture")]
pub capture_screenshot: Option<std::string::String>,
#[cfg(feature = "capture")]
pub capture_gif: Option<(std::string::String, u16)>, }
#[cfg(feature = "capture")]
impl Default for WindowConfig {
fn default() -> Self {
Self {
title: "Chart Example",
theme: WindowTheme::Default,
target_fps: 60,
auto_close: false,
background_color: Rgb565::WHITE,
size: Size::new(640, 480),
capture_screenshot: None,
capture_gif: None,
}
}
}
#[cfg(not(feature = "capture"))]
impl Default for WindowConfig {
fn default() -> Self {
Self {
title: "Chart Example",
theme: WindowTheme::Default,
target_fps: 60,
auto_close: false,
background_color: Rgb565::WHITE,
size: Size::new(640, 480),
}
}
}
#[allow(dead_code)] impl WindowConfig {
pub fn new(title: &'static str) -> Self {
Self {
title,
..Default::default()
}
}
pub fn theme(mut self, theme: WindowTheme) -> Self {
self.theme = theme;
self
}
pub fn fps(mut self, fps: u32) -> Self {
self.target_fps = fps;
self
}
pub fn auto_close(mut self) -> Self {
self.auto_close = true;
self
}
pub fn background(mut self, color: Rgb565) -> Self {
self.background_color = color;
self
}
pub fn size(mut self, size: Size) -> Self {
self.size = size;
self
}
pub fn viewport(&self) -> Rectangle {
Rectangle::new(
Point::new(20, 20),
Size::new(self.size.width - 40, self.size.height - 40),
)
}
pub fn frame_delay_ms(&self) -> u64 {
1000 / self.target_fps as u64
}
}
#[cfg(feature = "std")]
#[allow(dead_code)] pub struct WindowManager {
window: Window,
pub config: WindowConfig,
start_time: std::time::Instant,
#[cfg(feature = "capture")]
gif_capture: Option<GifCapture>,
}
#[cfg(all(feature = "std", feature = "capture"))]
#[allow(dead_code)]
impl WindowManager {
pub fn new(window_config: &WindowConfig) -> Self {
let title = window_config.title;
let output_settings = OutputSettingsBuilder::new()
.theme(window_config.theme.to_binary_color_theme())
.pixel_spacing(window_config.theme.pixel_spacing())
.scale(window_config.theme.scale())
.build();
let window = Window::new(title, &output_settings);
Self {
window,
config: window_config.clone(),
start_time: std::time::Instant::now(),
gif_capture: None,
}
}
}
#[cfg(all(feature = "std", not(feature = "capture")))]
#[allow(dead_code)]
impl WindowManager {
pub fn new(window_config: &WindowConfig) -> Self {
let title = window_config.title;
let output_settings = OutputSettingsBuilder::new()
.theme(window_config.theme.to_binary_color_theme())
.pixel_spacing(window_config.theme.pixel_spacing())
.scale(window_config.theme.scale())
.build();
let window = Window::new(title, &output_settings);
Self {
window,
config: window_config.clone(),
start_time: std::time::Instant::now(),
}
}
}
#[cfg(feature = "std")]
#[allow(dead_code)]
impl WindowManager {
pub fn update(&mut self, display: &SimulatorDisplay<Rgb565>) {
self.window.update(display);
}
pub fn should_close(&mut self) -> bool {
if self.window.events().any(|e| e == SimulatorEvent::Quit) {
return true;
}
if self.config.auto_close && self.start_time.elapsed().as_secs() > 5 {
return true;
}
false
}
}
#[cfg(all(feature = "std", feature = "capture"))]
#[allow(dead_code)]
impl WindowManager {
pub fn capture_screenshot<P: AsRef<std::path::Path>>(
&self,
display: &SimulatorDisplay<Rgb565>,
path: P,
) -> Result<(), Box<dyn std::error::Error>> {
capture_screenshot(display, path)
}
pub fn start_gif_capture(&mut self, delay_ms: u16) {
self.gif_capture = Some(GifCapture::new(delay_ms));
}
pub fn add_gif_frame(&mut self, display: &SimulatorDisplay<Rgb565>) {
if let Some(ref mut gif_capture) = self.gif_capture {
gif_capture.add_frame(display);
}
}
pub fn save_gif<P: AsRef<std::path::Path>>(
&mut self,
path: P,
) -> Result<(), Box<dyn std::error::Error>> {
if let Some(gif_capture) = self.gif_capture.take() {
gif_capture.save_gif(path)?;
}
Ok(())
}
}
#[cfg(feature = "std")]
#[allow(dead_code)] pub fn run<F>(window_config: WindowConfig, mut animation_fn: F) -> ChartResult<()>
where
F: FnMut(&mut SimulatorDisplay<Rgb565>, Rectangle, f32) -> ChartResult<()>, {
let mut display = SimulatorDisplay::new(window_config.size);
let viewport = window_config.viewport();
let background_color = window_config.background_color;
let mut window_manager = WindowManager::new(&window_config);
let start_time = std::time::Instant::now();
#[cfg(feature = "capture")]
let mut captured_screenshot = false;
#[cfg(feature = "capture")]
let mut gif_capture_started = false;
window_manager.update(&display);
loop {
if window_manager.should_close() {
#[cfg(feature = "capture")]
if gif_capture_started {
std::fs::create_dir_all("docs/assets").ok();
let gif_filename = format!(
"docs/assets/{}.gif",
window_manager.config.title.replace(" ", "_").to_lowercase()
);
if window_manager.save_gif(&gif_filename).is_ok() {
println!("✅ Animation saved to {gif_filename}");
}
}
break;
}
display
.clear(background_color)
.map_err(|_| ChartError::RenderingError)?;
let elapsed = start_time.elapsed().as_secs_f32();
animation_fn(&mut display, viewport, elapsed)?;
#[cfg(feature = "capture")]
{
let is_animated = window_manager
.config
.title
.to_lowercase()
.contains("animation")
|| window_manager
.config
.title
.to_lowercase()
.contains("animated")
|| window_manager
.config
.title
.to_lowercase()
.contains("streaming")
|| window_manager
.config
.title
.to_lowercase()
.contains("real-time")
|| window_manager
.config
.title
.to_lowercase()
.contains("dashboard")
|| window_manager.config.title.to_lowercase().contains("demo");
if !gif_capture_started && elapsed > 0.5 && is_animated {
window_manager.start_gif_capture(100); gif_capture_started = true;
println!("🎬 Starting GIF capture for animated example...");
}
if gif_capture_started {
window_manager.add_gif_frame(&display);
}
if !captured_screenshot && elapsed > 1.0 && !is_animated {
std::fs::create_dir_all("docs/assets").ok();
let filename = format!(
"docs/assets/{}.png",
window_manager.config.title.replace(" ", "_").to_lowercase()
);
if window_manager
.capture_screenshot(&display, &filename)
.is_ok()
{
println!("✅ Screenshot saved to {filename}");
captured_screenshot = true;
}
}
}
window_manager.update(&display);
std::thread::sleep(std::time::Duration::from_millis(
window_manager.config.frame_delay_ms(),
));
}
Ok(())
}
#[cfg(feature = "std")]
#[allow(dead_code)] pub fn run_static<F>(window_config: WindowConfig, mut render_fn: F) -> ChartResult<()>
where
F: FnMut(&mut SimulatorDisplay<Rgb565>, Rectangle) -> ChartResult<()>,
{
let mut display = SimulatorDisplay::new(window_config.size);
let viewport = window_config.viewport();
let background_color = window_config.background_color;
display
.clear(background_color)
.map_err(|_| ChartError::RenderingError)?;
render_fn(&mut display, viewport)?;
#[cfg(feature = "capture")]
{
std::fs::create_dir_all("docs/assets").ok();
let filename = format!(
"docs/assets/{}.png",
window_config.title.replace(" ", "_").to_lowercase()
);
if capture::capture_screenshot(&display, &filename).is_ok() {
println!("✅ Screenshot saved to {filename}");
}
}
let mut window_manager = WindowManager::new(&window_config);
loop {
window_manager.update(&display);
if window_manager.should_close() {
break;
}
std::thread::sleep(std::time::Duration::from_millis(
window_manager.config.frame_delay_ms(),
));
}
Ok(())
}
#[cfg(not(feature = "std"))]
pub struct WindowManager;
#[cfg(not(feature = "std"))]
#[allow(dead_code)]
impl WindowManager {
pub fn new(_window_config: &WindowConfig) -> Self {
Self
}
}
#[cfg(not(feature = "std"))]
pub fn run<F>(_window_config: WindowConfig, _animation_fn: F) -> ChartResult<()>
where
F: FnMut(
&mut embedded_graphics::mock_display::MockDisplay<Rgb565>,
Rectangle,
f32,
) -> ChartResult<()>,
{
println!("Animated examples require the 'std' feature to run with the simulator");
Ok(())
}
#[allow(dead_code)] pub mod presets {
use super::*;
pub fn default(title: &'static str) -> WindowConfig {
WindowConfig::new(title)
}
pub fn dark_theme(title: &'static str) -> WindowConfig {
WindowConfig::new(title).theme(WindowTheme::Dark)
}
pub fn oled_theme(title: &'static str) -> WindowConfig {
WindowConfig::new(title).theme(WindowTheme::OledBlue)
}
pub fn performance(title: &'static str) -> WindowConfig {
WindowConfig::new(title).fps(30)
}
pub fn demo(title: &'static str) -> WindowConfig {
WindowConfig::new(title).auto_close()
}
pub fn scaled(title: &'static str, scale: u32) -> WindowConfig {
WindowConfig::new(title).theme(WindowTheme::Custom {
pixel_spacing: 1,
scale,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_window_config() {
let config = WindowConfig::new("Test Window")
.theme(WindowTheme::Dark)
.fps(30)
.background(Rgb565::BLACK);
assert_eq!(config.title, "Test Window");
assert_eq!(config.target_fps, 30);
assert_eq!(config.background_color, Rgb565::BLACK);
assert_eq!(config.frame_delay_ms(), 33); }
#[test]
fn test_window_theme() {
let theme = WindowTheme::Custom {
pixel_spacing: 2,
scale: 3,
};
assert_eq!(theme.pixel_spacing(), 2);
assert_eq!(theme.scale(), 3);
}
#[test]
fn test_preset_configs() {
let config = presets::dark_theme("Dark Chart");
assert_eq!(config.title, "Dark Chart");
let demo_config = presets::demo("Demo Chart");
assert!(demo_config.auto_close);
}
}