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:
+2
-2
@@ -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
@@ -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
@@ -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};
|
||||||
|
|||||||
@@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
+70
-29
@@ -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,14 +39,14 @@ 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
|
||||||
@@ -60,7 +61,7 @@ fn default_widget_borders() -> WidgetBorderColors {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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
|
||||||
@@ -73,7 +74,7 @@ fn default_warning_colors() -> WarningColors {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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
|
||||||
@@ -86,7 +87,7 @@ fn default_info_box_colors() -> InfoBoxColors {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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
|
||||||
@@ -99,20 +100,20 @@ fn default_create_secret_info_colors() -> CreateSecretInfoColors {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+96
-44
@@ -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,10 +42,13 @@ 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> {
|
||||||
|
#[cfg(feature = "embedded_bg")]
|
||||||
|
{
|
||||||
tracing::info!("Loading embedded default background");
|
tracing::info!("Loading embedded default background");
|
||||||
match image::load_from_memory(DEFAULT_BACKGROUND) {
|
match image::load_from_memory(DEFAULT_BACKGROUND) {
|
||||||
Ok(img) => {
|
Ok(img) => {
|
||||||
@@ -48,7 +58,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(
|
||||||
"embedded_background",
|
"embedded_background",
|
||||||
color_image,
|
color_image,
|
||||||
TextureOptions::LINEAR,
|
TextureOptions::LINEAR,
|
||||||
@@ -62,6 +72,12 @@ impl BackgroundRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#[cfg(not(feature = "embedded_bg"))]
|
||||||
|
{
|
||||||
|
tracing::info!("Embedded background not available, will use fallback color");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
fn load_texture(ctx: &Context, path: &str) -> Option<TextureHandle> {
|
fn load_texture(ctx: &Context, path: &str) -> Option<TextureHandle> {
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user