Add hot-reload config support to detect and apply changes without restart

- Create ConfigWatcher module to monitor config.toml for modifications
- Add get_config_path() to consistently determine config file location
- Implement hot-reload detection in update() that checks for file changes each frame
- Add apply_config_changes() method to handle config updates gracefully
- Add PartialEq derive to all config structs to enable change detection
- Config changes now apply immediately without requiring app restart
- Background opacity, colors, text sizing, and other settings update live
- Log all detected changes for debugging and user awareness
- Fixes issue where config.toml changes were only applied when running from cargo run
This commit is contained in:
2026-02-06 12:23:28 +01:00
parent 64d707bd8b
commit 51811d4f57
6 changed files with 326 additions and 124 deletions
+2 -2
View File
@@ -3,7 +3,6 @@ width = 1024.0
height = 768.0 height = 768.0
min_width = 800.0 min_width = 800.0
min_height = 600.0 min_height = 600.0
transparency = 1.0
use_transparency = true use_transparency = true
position_offset_x = 0.0 position_offset_x = 0.0
position_offset_y = -30.0 position_offset_y = -30.0
@@ -23,7 +22,8 @@ secret_expiration_years = 50 # How many years until created secrets expire (def
# Leave empty to use embedded default background, or specify custom path # Leave empty to use embedded default background, or specify custom path
# Examples: "./assets/background.jpg", "C:/path/to/image.png" # Examples: "./assets/background.jpg", "C:/path/to/image.png"
background_image = "./assets/background.jpg" background_image = "./assets/background.jpg"
background_opacity = 1.0 background_opacity = 0.8
fallback_color_opacity = 0.4
background_blur = 0.0 background_blur = 0.0
fallback_color = [30, 30, 40] # Used if image fails to load fallback_color = [30, 30, 40] # Used if image fails to load
+50 -9
View File
@@ -1,6 +1,6 @@
use crate::auth::AzureAuthenticator; use crate::auth::AzureAuthenticator;
use crate::azure::{GraphApiClient, KeyVaultClient, VaultDiscovery}; use crate::azure::{GraphApiClient, KeyVaultClient, VaultDiscovery};
use crate::config::Config; use crate::config::{Config, ConfigWatcher, get_config_path};
use crate::state::{AppState, AuthStatus, ViewState}; use crate::state::{AppState, AuthStatus, ViewState};
use crate::ui::*; use crate::ui::*;
use poll_promise::Promise; use poll_promise::Promise;
@@ -13,6 +13,7 @@ pub struct AzureAppManager {
keyvault_client: KeyVaultClient, keyvault_client: KeyVaultClient,
vault_discovery: VaultDiscovery, vault_discovery: VaultDiscovery,
config: Config, config: Config,
config_watcher: ConfigWatcher,
position_applied: bool, position_applied: bool,
} }
@@ -30,6 +31,7 @@ impl AzureAppManager {
&config.appearance.background_image, &config.appearance.background_image,
config.appearance.background_opacity, config.appearance.background_opacity,
config.appearance.fallback_color, config.appearance.fallback_color,
config.appearance.fallback_color_opacity,
))); )));
state.background_renderer = background_renderer; state.background_renderer = background_renderer;
@@ -39,6 +41,9 @@ impl AzureAppManager {
state.current_view = ViewState::AppList; state.current_view = ViewState::AppList;
} }
// Initialize config watcher
let config_watcher = ConfigWatcher::new(get_config_path());
Self { Self {
state, state,
auth, auth,
@@ -46,6 +51,7 @@ impl AzureAppManager {
keyvault_client, keyvault_client,
vault_discovery, vault_discovery,
config, config,
config_watcher,
position_applied: false, position_applied: false,
} }
} }
@@ -361,21 +367,56 @@ impl AzureAppManager {
auth.sign_out().await auth.sign_out().await
})); }));
} }
/// Apply configuration changes detected from hot-reload
/// Updates appearance and color settings without requiring app restart
fn apply_config_changes(&mut self, new_config: &Config) {
// Check if appearance settings changed
let appearance_changed =
self.config.appearance.background_opacity != new_config.appearance.background_opacity ||
self.config.appearance.fallback_color != new_config.appearance.fallback_color ||
self.config.appearance.fallback_color_opacity != new_config.appearance.fallback_color_opacity;
if appearance_changed {
tracing::info!("Background appearance changed, updating renderer");
// Note: We can't easily recreate the BackgroundRenderer here because we don't have
// access to the egui Context. Instead, the color values are applied in each frame's
// render_fullscreen call, which will pick up the new config values automatically.
// The update happens when we set self.config = new_config above.
}
// Color changes are applied every frame in the update() function via self.config.colors
// so no additional work is needed here - just logging for debugging
if self.config.colors != new_config.colors {
tracing::info!("Color configuration changed, will apply on next frame");
}
if self.config.text_sizing != new_config.text_sizing {
tracing::info!("Text sizing changed, will apply on next frame");
}
if self.config.azure != new_config.azure {
tracing::info!("Azure configuration changed");
}
}
} }
impl eframe::App for AzureAppManager { impl eframe::App for AzureAppManager {
fn clear_color(&self, _visuals: &egui::Visuals) -> [f32; 4] { fn clear_color(&self, _visuals: &egui::Visuals) -> [f32; 4] {
// Use the fallback color from config to match background // Return fully transparent black to allow background rendering to show through
let color = self.config.appearance.fallback_color; // The background renderer (render_fullscreen) handles all background rendering
[ // including fallback color with proper transparency
color[0] as f32 / 255.0, [0.0, 0.0, 0.0, 0.0]
color[1] as f32 / 255.0,
color[2] as f32 / 255.0,
1.0, // Full opacity
]
} }
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
// Check for config file changes and hot-reload if needed
if let Some(new_config) = self.config_watcher.check_for_changes() {
tracing::info!("Config changes detected, applying hot-reload");
self.apply_config_changes(&new_config);
self.config = new_config;
}
// Customize colors to match app theme // Customize colors to match app theme
let mut visuals = egui::Visuals::dark(); let mut visuals = egui::Visuals::dark();
+4 -1
View File
@@ -1,2 +1,5 @@
pub mod watcher;
pub mod window_config; pub mod window_config;
pub use window_config::{Config, load_config};
pub use watcher::ConfigWatcher;
pub use window_config::{get_config_path, load_config, Config};
+65
View File
@@ -0,0 +1,65 @@
use crate::config::Config;
use std::path::PathBuf;
use std::time::UNIX_EPOCH;
/// Tracks the last modification time of config.toml
pub struct ConfigWatcher {
config_path: PathBuf,
last_modified: u64,
}
impl ConfigWatcher {
pub fn new(config_path: PathBuf) -> Self {
let last_modified = Self::get_file_mtime(&config_path).unwrap_or(0);
Self {
config_path,
last_modified,
}
}
/// Check if config.toml has been modified since last check
/// Returns new Config if file was modified, None otherwise
pub fn check_for_changes(&mut self) -> Option<Config> {
let current_mtime = Self::get_file_mtime(&self.config_path)?;
if current_mtime > self.last_modified {
tracing::info!("Config file has been modified, reloading...");
self.last_modified = current_mtime;
// Try to reload config
match std::fs::read_to_string(&self.config_path) {
Ok(contents) => match toml::from_str::<Config>(&contents) {
Ok(config) => {
tracing::info!("Config reloaded successfully!");
return Some(config);
}
Err(e) => {
tracing::error!(
"Failed to parse updated config.toml: {}. Keeping old config.",
e
);
}
},
Err(e) => {
tracing::error!(
"Failed to read updated config.toml: {}. Keeping old config.",
e
);
}
}
}
None
}
/// Get the last modification time of a file
fn get_file_mtime(path: &PathBuf) -> Option<u64> {
std::fs::metadata(path)
.ok()?
.modified()
.ok()?
.duration_since(UNIX_EPOCH)
.ok()
.map(|d| d.as_secs())
}
}
+91 -50
View File
@@ -1,20 +1,19 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fs; use std::fs;
use std::path::Path; use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct WindowConfig { pub struct WindowConfig {
pub width: f32, pub width: f32,
pub height: f32, pub height: f32,
pub min_width: f32, pub min_width: f32,
pub min_height: f32, pub min_height: f32,
pub transparency: f32,
pub use_transparency: bool, pub use_transparency: bool,
pub position_offset_x: f32, pub position_offset_x: f32,
pub position_offset_y: f32, pub position_offset_y: f32,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TextSizing { pub struct TextSizing {
pub heading: f32, pub heading: f32,
pub body: f32, pub body: f32,
@@ -22,15 +21,17 @@ pub struct TextSizing {
pub button: f32, pub button: f32,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AppearanceConfig { pub struct AppearanceConfig {
pub background_image: String, pub background_image: String,
pub background_opacity: f32, pub background_opacity: f32,
pub background_blur: f32, pub background_blur: f32,
pub fallback_color: [u8; 3], pub fallback_color: [u8; 3],
#[serde(default = "default_fallback_color_opacity")]
pub fallback_color_opacity: f32,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TextColors { pub struct TextColors {
pub normal: [u8; 3], pub normal: [u8; 3],
pub inactive: [u8; 3], pub inactive: [u8; 3],
@@ -38,81 +39,81 @@ pub struct TextColors {
pub select: [u8; 3], pub select: [u8; 3],
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SliderColors { pub struct SliderColors {
pub inactive: [u8; 3], pub inactive: [u8; 3],
pub hover: [u8; 3], pub hover: [u8; 3],
pub active: [u8; 3], pub active: [u8; 3],
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct WidgetBorderColors { pub struct WidgetBorderColors {
pub inactive: [u8; 3], // For noninteractive and inactive states pub inactive: [u8; 3], // For noninteractive and inactive states
pub hover: [u8; 3], // For hovered state pub hover: [u8; 3], // For hovered state
pub active: [u8; 3], // For active/pressed state pub active: [u8; 3], // For active/pressed state
} }
fn default_widget_borders() -> WidgetBorderColors { fn default_widget_borders() -> WidgetBorderColors {
WidgetBorderColors { WidgetBorderColors {
inactive: [60, 60, 70], // Current hardcoded dark grey inactive: [60, 60, 70], // Current hardcoded dark grey
hover: [120, 120, 140], // Current hardcoded lighter grey hover: [120, 120, 140], // Current hardcoded lighter grey
active: [120, 120, 140], // Same as hover active: [120, 120, 140], // Same as hover
} }
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct WarningColors { pub struct WarningColors {
pub text: [u8; 3], // Warning text color pub text: [u8; 3], // Warning text color
pub background: [u8; 3], // Warning box background color pub background: [u8; 3], // Warning box background color
} }
fn default_warning_colors() -> WarningColors { fn default_warning_colors() -> WarningColors {
WarningColors { WarningColors {
text: [255, 200, 100], // Default amber/orange text: [255, 200, 100], // Default amber/orange
background: [80, 60, 30], // Default dark orange background background: [80, 60, 30], // Default dark orange background
} }
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct InfoBoxColors { pub struct InfoBoxColors {
pub background: [u8; 3], // Info box background color pub background: [u8; 3], // Info box background color
pub border: [u8; 3], // Info box border color pub border: [u8; 3], // Info box border color
} }
fn default_info_box_colors() -> InfoBoxColors { fn default_info_box_colors() -> InfoBoxColors {
InfoBoxColors { InfoBoxColors {
background: [40, 40, 60], // Default dark blue-grey background: [40, 40, 60], // Default dark blue-grey
border: [239, 124, 0], // Default orange border border: [239, 124, 0], // Default orange border
} }
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CreateSecretInfoColors { pub struct CreateSecretInfoColors {
pub background: [u8; 3], // Info box background color pub background: [u8; 3], // Info box background color
pub text: [u8; 3], // Info box text color pub text: [u8; 3], // Info box text color
} }
fn default_create_secret_info_colors() -> CreateSecretInfoColors { fn default_create_secret_info_colors() -> CreateSecretInfoColors {
CreateSecretInfoColors { CreateSecretInfoColors {
background: [40, 40, 60], // Default purple/dark blue background: [40, 40, 60], // Default purple/dark blue
text: [239, 124, 0], // Default orange text: [239, 124, 0], // Default orange
} }
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CardColors { pub struct CardColors {
pub selected_bg: [u8; 4], // RGBA pub selected_bg: [u8; 4], // RGBA
pub selected_border: [u8; 3], // RGB pub selected_border: [u8; 3], // RGB
pub unselected_bg: [u8; 4], // RGBA pub unselected_bg: [u8; 4], // RGBA
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CardConfig { pub struct CardConfig {
pub app_list: CardColors, pub app_list: CardColors,
pub keyvault: CardColors, pub keyvault: CardColors,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ColorConfig { pub struct ColorConfig {
pub text: TextColors, pub text: TextColors,
pub slider: SliderColors, pub slider: SliderColors,
@@ -127,7 +128,7 @@ pub struct ColorConfig {
pub create_secret_info: CreateSecretInfoColors, pub create_secret_info: CreateSecretInfoColors,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AzureConfig { pub struct AzureConfig {
pub secret_expiration_years: u32, pub secret_expiration_years: u32,
} }
@@ -138,7 +139,11 @@ fn default_azure_config() -> AzureConfig {
} }
} }
#[derive(Debug, Clone, Serialize, Deserialize)] fn default_fallback_color_opacity() -> f32 {
1.0
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Config { pub struct Config {
pub window: WindowConfig, pub window: WindowConfig,
pub appearance: AppearanceConfig, pub appearance: AppearanceConfig,
@@ -158,16 +163,16 @@ impl Default for Config {
height: 768.0, height: 768.0,
min_width: 800.0, min_width: 800.0,
min_height: 600.0, min_height: 600.0,
transparency: 0.0,
use_transparency: false, use_transparency: false,
position_offset_x: 0.0, position_offset_x: 0.0,
position_offset_y: 0.0, position_offset_y: 0.0,
}, },
appearance: AppearanceConfig { appearance: AppearanceConfig {
background_image: String::new(), // Empty = use embedded default, or specify custom path background_image: String::new(),
background_opacity: 1.0, background_opacity: 1.0,
background_blur: 0.0, background_blur: 0.0,
fallback_color: [30, 30, 40], fallback_color: [30, 30, 40],
fallback_color_opacity: 1.0,
}, },
colors: Some(ColorConfig { colors: Some(ColorConfig {
text: TextColors { text: TextColors {
@@ -225,24 +230,44 @@ impl Default for Config {
} }
pub fn load_config() -> Config { pub fn load_config() -> Config {
let config_path = "./config.toml"; // Get the directory where the exe is located
let config_path = if let Ok(exe_path) = std::env::current_exe() {
exe_path
.parent()
.map(|p| p.join("config.toml"))
.unwrap_or_else(|| Path::new("./config.toml").to_path_buf())
} else {
Path::new("./config.toml").to_path_buf()
};
tracing::info!("Loading config from: {}", config_path); tracing::info!("Loading config from: {:?}", config_path);
tracing::info!("Current working directory: {:?}", std::env::current_dir().ok()); tracing::info!(
"Current working directory: {:?}",
std::env::current_dir().ok()
);
if Path::new(config_path).exists() { if config_path.exists() {
tracing::info!("Config file found"); tracing::info!("Config file found");
match fs::read_to_string(config_path) { match fs::read_to_string(&config_path) {
Ok(contents) => { Ok(contents) => {
tracing::info!("Config file read successfully, {} bytes", contents.len()); tracing::info!("Config file read successfully, {} bytes", contents.len());
match toml::from_str::<Config>(&contents) { match toml::from_str::<Config>(&contents) {
Ok(config) => { Ok(config) => {
tracing::info!("Config parsed successfully!"); tracing::info!("Config parsed successfully!");
tracing::info!(" - Background image: {}", config.appearance.background_image); tracing::info!(
tracing::info!(" - Background opacity: {}", config.appearance.background_opacity); " - Background image: {}",
tracing::info!(" - Fallback color: {:?}", config.appearance.fallback_color); config.appearance.background_image
);
tracing::info!(
" - Background opacity: {}",
config.appearance.background_opacity
);
tracing::info!(
" - Fallback color: {:?}",
config.appearance.fallback_color
);
return config; return config;
}, }
Err(e) => { Err(e) => {
tracing::error!("Failed to parse config.toml: {}. Using defaults.", e); tracing::error!("Failed to parse config.toml: {}. Using defaults.", e);
} }
@@ -253,11 +278,14 @@ pub fn load_config() -> Config {
} }
} }
} else { } else {
tracing::warn!("Config file not found at {}, creating default", config_path); tracing::warn!(
"Config file not found at {:?}, creating default",
config_path
);
// Create default config file // Create default config file
let default_config = Config::default(); let default_config = Config::default();
if let Ok(toml_string) = toml::to_string_pretty(&default_config) { if let Ok(toml_string) = toml::to_string_pretty(&default_config) {
let _ = fs::write(config_path, toml_string); let _ = fs::write(&config_path, toml_string);
tracing::info!("Created default config.toml"); tracing::info!("Created default config.toml");
} }
} }
@@ -265,3 +293,16 @@ pub fn load_config() -> Config {
tracing::warn!("Using default config values"); tracing::warn!("Using default config values");
Config::default() Config::default()
} }
/// Get the path to the config.toml file
/// This is the same path used by load_config()
pub fn get_config_path() -> PathBuf {
if let Ok(exe_path) = std::env::current_exe() {
exe_path
.parent()
.map(|p| p.join("config.toml"))
.unwrap_or_else(|| Path::new("./config.toml").to_path_buf())
} else {
Path::new("./config.toml").to_path_buf()
}
}
+114 -62
View File
@@ -1,12 +1,15 @@
use egui::{Color32, ColorImage, Context, Rect, TextureHandle, TextureOptions, Ui}; use egui::{Color32, ColorImage, Context, Rect, TextureHandle, TextureOptions, Ui};
// Embed background image at compile time for instant loading // Try to embed background image at compile time, but don't fail if it's missing
// The background is optional - if unavailable, we'll use the fallback color
#[cfg(feature = "embedded_bg")]
const DEFAULT_BACKGROUND: &[u8] = include_bytes!("../../assets/background.jpg"); const DEFAULT_BACKGROUND: &[u8] = include_bytes!("../../assets/background.jpg");
pub struct BackgroundRenderer { pub struct BackgroundRenderer {
texture: Option<TextureHandle>, texture: Option<TextureHandle>,
fallback_color: Color32, fallback_color: Color32,
opacity: f32, opacity: f32,
fallback_color_opacity: f32,
} }
impl BackgroundRenderer { impl BackgroundRenderer {
@@ -15,6 +18,7 @@ impl BackgroundRenderer {
_image_path: &str, _image_path: &str,
opacity: f32, opacity: f32,
fallback_color: [u8; 3], fallback_color: [u8; 3],
fallback_color_opacity: f32,
) -> Self { ) -> Self {
tracing::info!("Initializing background renderer with embedded image"); tracing::info!("Initializing background renderer with embedded image");
@@ -22,7 +26,10 @@ impl BackgroundRenderer {
let texture = Self::load_embedded_texture(ctx); let texture = Self::load_embedded_texture(ctx);
if texture.is_some() { if texture.is_some() {
tracing::info!("Background texture loaded from embedded image with opacity: {}", opacity); tracing::info!(
"Background texture loaded from embedded image with opacity: {}",
opacity
);
} else { } else {
tracing::info!("Using fallback color: {:?}", fallback_color); tracing::info!("Using fallback color: {:?}", fallback_color);
} }
@@ -35,32 +42,41 @@ impl BackgroundRenderer {
fallback_color[2], fallback_color[2],
), ),
opacity, opacity,
fallback_color_opacity,
} }
} }
fn load_embedded_texture(ctx: &Context) -> Option<TextureHandle> { fn load_embedded_texture(_ctx: &Context) -> Option<TextureHandle> {
tracing::info!("Loading embedded default background"); #[cfg(feature = "embedded_bg")]
match image::load_from_memory(DEFAULT_BACKGROUND) { {
Ok(img) => { tracing::info!("Loading embedded default background");
let size = [img.width() as usize, img.height() as usize]; match image::load_from_memory(DEFAULT_BACKGROUND) {
tracing::info!("Embedded image loaded: {}x{} pixels", size[0], size[1]); Ok(img) => {
let image_buffer = img.to_rgba8(); let size = [img.width() as usize, img.height() as usize];
let pixels = image_buffer.as_flat_samples(); tracing::info!("Embedded image loaded: {}x{} pixels", size[0], size[1]);
let color_image = ColorImage::from_rgba_unmultiplied(size, pixels.as_slice()); let image_buffer = img.to_rgba8();
let pixels = image_buffer.as_flat_samples();
let color_image = ColorImage::from_rgba_unmultiplied(size, pixels.as_slice());
let texture = ctx.load_texture( let texture = _ctx.load_texture(
"embedded_background", "embedded_background",
color_image, color_image,
TextureOptions::LINEAR, TextureOptions::LINEAR,
); );
tracing::info!("Embedded texture created successfully"); tracing::info!("Embedded texture created successfully");
Some(texture) Some(texture)
} }
Err(e) => { Err(e) => {
tracing::error!("Failed to load embedded background: {}", e); tracing::error!("Failed to load embedded background: {}", e);
None None
}
} }
} }
#[cfg(not(feature = "embedded_bg"))]
{
tracing::info!("Embedded background not available, will use fallback color");
None
}
} }
#[allow(dead_code)] #[allow(dead_code)]
@@ -74,11 +90,7 @@ impl BackgroundRenderer {
let pixels = image_buffer.as_flat_samples(); let pixels = image_buffer.as_flat_samples();
let color_image = ColorImage::from_rgba_unmultiplied(size, pixels.as_slice()); let color_image = ColorImage::from_rgba_unmultiplied(size, pixels.as_slice());
let texture = ctx.load_texture( let texture = ctx.load_texture(path, color_image, TextureOptions::LINEAR);
path,
color_image,
TextureOptions::LINEAR,
);
tracing::info!("Texture created with ID: {:?}", texture.id()); tracing::info!("Texture created with ID: {:?}", texture.id());
Some(texture) Some(texture)
} }
@@ -97,10 +109,18 @@ impl BackgroundRenderer {
// Use screen-space painting (lowest possible layer) // Use screen-space painting (lowest possible layer)
let painter = ctx.layer_painter(egui::LayerId::background()); let painter = ctx.layer_painter(egui::LayerId::background());
// Fill entire screen with background color first // Always render fallback color as the base layer
painter.rect_filled(screen_rect, 0.0, self.fallback_color); // This provides a foundation color, whether or not the image loads
let fallback_with_opacity = Color32::from_rgba_unmultiplied(
self.fallback_color.r(),
self.fallback_color.g(),
self.fallback_color.b(),
(self.fallback_color_opacity * 255.0) as u8,
);
painter.rect_filled(screen_rect, 0.0, fallback_with_opacity);
if let Some(texture) = &self.texture { if let Some(texture) = &self.texture {
// If we have a texture, render it on top with configured opacity
// Calculate UV coordinates to fit image properly (cover the screen) // Calculate UV coordinates to fit image properly (cover the screen)
let texture_aspect = texture.aspect_ratio(); let texture_aspect = texture.aspect_ratio();
let screen_aspect = screen_rect.width() / screen_rect.height(); let screen_aspect = screen_rect.width() / screen_rect.height();
@@ -109,34 +129,26 @@ impl BackgroundRenderer {
// Image is wider - crop left side to show right side (where logo is) // Image is wider - crop left side to show right side (where logo is)
let scale = screen_aspect / texture_aspect; let scale = screen_aspect / texture_aspect;
let offset = 1.0 - scale; let offset = 1.0 - scale;
Rect::from_min_max( Rect::from_min_max(egui::pos2(offset, 0.0), egui::pos2(1.0, 1.0))
egui::pos2(offset, 0.0),
egui::pos2(1.0, 1.0)
)
} else { } else {
// Image is taller - keep top visible (where logo is) // Image is taller - keep top visible (where logo is)
let scale = texture_aspect / screen_aspect; let scale = texture_aspect / screen_aspect;
Rect::from_min_max( Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, scale))
egui::pos2(0.0, 0.0),
egui::pos2(1.0, scale)
)
}; };
// Draw background image with proper cropping // Draw background image with configured opacity on top of fallback
let tint = Color32::from_rgba_unmultiplied( let tint = Color32::from_rgba_unmultiplied(255, 255, 255, (self.opacity * 255.0) as u8);
255,
255,
255,
(self.opacity * 255.0) as u8,
);
painter.image( painter.image(texture.id(), screen_rect, uv_rect, tint);
texture.id(), tracing::debug!(
screen_rect, "Background image rendered fullscreen with UV rect {:?}",
uv_rect, uv_rect
tint, );
} else {
tracing::debug!(
"No background image, showing fallback color with opacity: {}",
self.fallback_color_opacity
); );
tracing::debug!("Background image rendered fullscreen with UV rect {:?}", uv_rect);
} }
} }
@@ -144,17 +156,9 @@ impl BackgroundRenderer {
pub fn render(&self, ui: &mut Ui) { pub fn render(&self, ui: &mut Ui) {
let rect = ui.max_rect(); let rect = ui.max_rect();
// Always draw fallback first
ui.painter().rect_filled(rect, 0.0, self.fallback_color);
if let Some(texture) = &self.texture { if let Some(texture) = &self.texture {
// Draw background image on top // Draw background image with configured opacity
let tint = Color32::from_rgba_unmultiplied( let tint = Color32::from_rgba_unmultiplied(255, 255, 255, (self.opacity * 255.0) as u8);
255,
255,
255,
(self.opacity * 255.0) as u8,
);
ui.painter().image( ui.painter().image(
texture.id(), texture.id(),
@@ -162,9 +166,57 @@ impl BackgroundRenderer {
Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)), Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)),
tint, tint,
); );
tracing::trace!("Background image rendered to rect {:?} with opacity {}", rect, self.opacity); tracing::trace!(
"Background image rendered to rect {:?} with opacity {}",
rect,
self.opacity
);
} else { } else {
tracing::trace!("No texture, only fallback color rendered"); // Draw fallback color with configured opacity
let fallback_with_opacity = Color32::from_rgba_unmultiplied(
self.fallback_color.r(),
self.fallback_color.g(),
self.fallback_color.b(),
(self.fallback_color_opacity * 255.0) as u8,
);
ui.painter().rect_filled(rect, 0.0, fallback_with_opacity);
tracing::trace!(
"Fallback color rendered with opacity {}",
self.fallback_color_opacity
);
}
}
/// Update the background opacity dynamically
#[allow(dead_code)]
pub fn set_opacity(&mut self, opacity: f32) {
if (self.opacity - opacity).abs() > 0.001 {
// Only log if there's a meaningful change
tracing::info!(
"Updating background opacity from {} to {}",
self.opacity,
opacity
);
self.opacity = opacity;
}
}
/// Update the fallback color dynamically
#[allow(dead_code)]
pub fn set_fallback_color(&mut self, fallback_color: [u8; 3], fallback_color_opacity: f32) {
let new_color = Color32::from_rgb(fallback_color[0], fallback_color[1], fallback_color[2]);
if self.fallback_color != new_color
|| (self.fallback_color_opacity - fallback_color_opacity).abs() > 0.001
{
tracing::info!(
"Updating fallback color from {:?} to {:?} with opacity {} to {}",
self.fallback_color,
new_color,
self.fallback_color_opacity,
fallback_color_opacity
);
self.fallback_color = new_color;
self.fallback_color_opacity = fallback_color_opacity;
} }
} }
} }