use std::path::PathBuf;
use super::context::ControllerContext;
use super::feature::ControllerFeature;
use super::types::TabInfo;
use crate::error::Result;
use crate::gui::content::Content;
use crate::gui::effect::{PresetEffect, PresetEffectOptions};
use crate::gui::shape::WindowShape;
use crate::gui::window::{Position, Size, WindowCommand, WindowConfig, WindowLevel};
const ALL_SHAPES: &[(&str, WindowShape)] =
&[("Circle", WindowShape::Circle), ("Rectangle", WindowShape::Rectangle)];
const ALL_EFFECTS: &[PresetEffect] = &[
PresetEffect::RotatingHalo,
PresetEffect::PulseRipple,
PresetEffect::FlowingLight,
PresetEffect::StardustScatter,
PresetEffect::ElectricSpark,
PresetEffect::SmokeWisp,
PresetEffect::RainDrop,
PresetEffect::LaserBeam,
PresetEffect::LightningArc,
PresetEffect::MeteorShower,
PresetEffect::SonarPulse,
PresetEffect::MatrixRain,
PresetEffect::AuroraWave,
PresetEffect::OrbitRings,
PresetEffect::HeartbeatPulse,
PresetEffect::CosmicStrings,
PresetEffect::SilkRibbon,
];
pub struct FloatingWindowsFeature {
selected_effect: PresetEffect,
effect_options: PresetEffectOptions,
selected_shape: WindowShape,
new_window_size: u32,
new_window_x: f32,
new_window_y: f32,
flow_window_image_path: Option<PathBuf>,
pending_image_update_window: Option<(winit::window::WindowId, (u32, u32))>,
}
impl FloatingWindowsFeature {
pub fn new() -> Self {
Self {
selected_effect: PresetEffect::SilkRibbon,
effect_options: PresetEffectOptions::default(),
selected_shape: WindowShape::Circle,
new_window_size: 50,
new_window_x: 500.0,
new_window_y: 300.0,
flow_window_image_path: None,
pending_image_update_window: None,
}
}
fn open_image_picker_for_flow_window(&mut self) {
if let Some(path) =
rfd::FileDialog::new().add_filter("Images", &["png", "svg", "jpg", "jpeg"]).pick_file()
{
self.flow_window_image_path = Some(path);
}
}
fn create_window(&mut self, ctx: &mut ControllerContext) {
let content = if let Some(path) = &self.flow_window_image_path {
match Content::from_path_sized(path, self.new_window_size, self.new_window_size) {
Ok(content) => Some(content),
Err(e) => {
log::error!("Failed to load image: {}", e);
None
}
}
} else {
None
};
let config = WindowConfig {
id: None,
title: Some(ctx.registry.generate_name()),
position: Position::new(self.new_window_x as f64, self.new_window_y as f64),
size: Size::new(self.new_window_size, self.new_window_size),
effect_margin: 0.0,
shape: self.selected_shape.clone(),
draggable: true,
resizable: false,
click_through: false,
level: WindowLevel::AlwaysOnTop,
opacity: 1.0,
icon: None,
content,
widget_content: None,
effect: None,
show_animation: None,
hide_animation: None,
};
let _ = ctx.command_sender.send(WindowCommand::Create {
config,
effect: Some((self.selected_effect, self.effect_options.clone())),
});
self.new_window_x += 60.0;
if self.new_window_x > 1000.0 {
self.new_window_x = 500.0;
self.new_window_y += 60.0;
}
}
fn render_create_section(&mut self, ui: &mut egui::Ui, ctx: &mut ControllerContext) {
ui.heading("Create New Window");
ui.add_space(8.0);
ui.horizontal(|ui| {
ui.label("Effect:");
egui::ComboBox::from_id_salt("effect_selector")
.selected_text(format!("{:?}", self.selected_effect))
.show_ui(ui, |ui| {
for effect in ALL_EFFECTS {
ui.selectable_value(
&mut self.selected_effect,
*effect,
format!("{:?}", effect),
);
}
});
});
ui.add_space(4.0);
ui.collapsing("Effect Options", |ui| {
self.render_effect_options(ui);
});
ui.add_space(4.0);
ui.horizontal(|ui| {
ui.label("Shape:");
egui::ComboBox::from_id_salt("shape_selector")
.selected_text(format!("{:?}", self.selected_shape))
.show_ui(ui, |ui| {
for (name, shape) in ALL_SHAPES {
ui.selectable_value(&mut self.selected_shape, shape.clone(), *name);
}
});
});
ui.add_space(4.0);
ui.horizontal(|ui| {
ui.label("Image:");
if let Some(path) = &self.flow_window_image_path {
let filename = path
.file_name()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| "Unknown".to_string());
ui.label(&filename);
} else {
ui.label("None");
}
if ui.button("Browse...").clicked() {
self.open_image_picker_for_flow_window();
}
if self.flow_window_image_path.is_some() && ui.button("Clear").clicked() {
self.flow_window_image_path = None;
}
});
ui.add_space(4.0);
ui.horizontal(|ui| {
ui.label("Size:");
ui.add(egui::Slider::new(&mut self.new_window_size, 30..=200).suffix("px"));
});
ui.horizontal(|ui| {
ui.label("Position X:");
ui.add(egui::DragValue::new(&mut self.new_window_x).range(0.0..=2000.0));
ui.label("Y:");
ui.add(egui::DragValue::new(&mut self.new_window_y).range(0.0..=2000.0));
});
ui.add_space(8.0);
if ui.button("Create Window").clicked() {
self.create_window(ctx);
}
}
fn render_effect_options(&mut self, ui: &mut egui::Ui) {
ui.horizontal(|ui| {
ui.label("Intensity:");
ui.add(egui::Slider::new(&mut self.effect_options.intensity, 0.0..=1.0));
});
ui.horizontal(|ui| {
ui.label("Speed:");
ui.add(egui::Slider::new(&mut self.effect_options.speed, 0.1..=3.0));
});
match self.selected_effect {
PresetEffect::SilkRibbon => {
ui.horizontal(|ui| {
ui.label("Ribbon Count:");
let mut count = self.effect_options.ribbon_count as i32;
if ui.add(egui::Slider::new(&mut count, 1..=6)).changed() {
self.effect_options.ribbon_count = count as usize;
}
});
ui.horizontal(|ui| {
ui.label("Petal Amplitude:");
ui.add(egui::Slider::new(&mut self.effect_options.petal_amplitude, 5.0..=50.0));
});
}
_ => {
ui.horizontal(|ui| {
ui.label("Edge Width:");
ui.add(egui::Slider::new(&mut self.effect_options.edge_width, 5.0..=30.0));
});
}
}
ui.horizontal(|ui| {
ui.label("Colors:");
if ui.button("Cyan/Purple").clicked() {
self.effect_options.particle_colors =
vec![[0.4, 0.8, 1.0, 1.0], [0.8, 0.4, 1.0, 1.0]];
}
if ui.button("Fire").clicked() {
self.effect_options.particle_colors =
vec![[1.0, 0.3, 0.0, 1.0], [1.0, 0.6, 0.0, 1.0], [1.0, 0.9, 0.2, 1.0]];
}
if ui.button("Rainbow").clicked() {
self.effect_options.particle_colors = vec![
[1.0, 0.0, 0.0, 1.0],
[1.0, 0.5, 0.0, 1.0],
[1.0, 1.0, 0.0, 1.0],
[0.0, 1.0, 0.0, 1.0],
[0.0, 0.5, 1.0, 1.0],
[0.5, 0.0, 1.0, 1.0],
];
}
});
}
fn render_manage_section(&mut self, ui: &mut egui::Ui, ctx: &mut ControllerContext) {
ui.heading("Managed Windows");
ui.add_space(8.0);
let windows = ctx.registry.list();
if windows.is_empty() {
ui.label("No windows created yet.");
} else {
egui::Grid::new("window_grid").num_columns(3).spacing([20.0, 4.0]).striped(true).show(
ui,
|ui| {
ui.label("Name");
ui.label("Effect");
ui.label("Actions");
ui.end_row();
for window in &windows {
ui.label(&window.name);
ui.label(format!(
"{:?}",
window.effect.unwrap_or(PresetEffect::RotatingHalo)
));
ui.horizontal(|ui| {
if ui.button("Close").clicked() {
let _ =
ctx.command_sender.send(WindowCommand::Close { id: window.id });
}
if ui.button("Set Image").clicked() {
self.pending_image_update_window = Some((window.id, window.size));
}
});
ui.end_row();
}
},
);
}
if let Some((window_id, size)) = self.pending_image_update_window.take() {
if let Some(path) = rfd::FileDialog::new()
.add_filter("Images", &["png", "svg", "jpg", "jpeg"])
.pick_file()
{
match Content::from_path_sized(&path, size.0, size.1) {
Ok(content) => {
let _ = ctx.command_sender.send(WindowCommand::UpdateContent {
id: window_id,
content: Some(content),
});
}
Err(e) => {
log::error!("Failed to load image: {}", e);
}
}
}
}
}
}
impl Default for FloatingWindowsFeature {
fn default() -> Self {
Self::new()
}
}
impl ControllerFeature for FloatingWindowsFeature {
fn id(&self) -> &'static str {
"floating_windows"
}
fn tab_info(&self) -> TabInfo {
TabInfo::new("floating_windows", "Floating Windows", 0) }
fn render(&mut self, ui: &mut egui::Ui, ctx: &mut ControllerContext) {
self.render_create_section(ui, ctx);
ui.add_space(16.0);
ui.separator();
ui.add_space(8.0);
self.render_manage_section(ui, ctx);
}
fn initialize(&mut self, _ctx: &mut ControllerContext) -> Result<()> {
log::info!("Floating windows feature initialized");
Ok(())
}
}