use eframe::egui::{self, Color32, Stroke, Vec2}; use std::path::PathBuf; pub struct DropZone { is_hovering: bool, } impl DropZone { pub fn new() -> Self { Self { is_hovering: false } } pub fn show(&mut self, ui: &mut egui::Ui, mut on_files_dropped: F) where F: FnMut(Vec), { let available_width = ui.available_width(); let height = 140.0; // Check for drag-and-drop hover state let is_hovering = ui.ctx().input(|i| !i.raw.hovered_files.is_empty()); self.is_hovering = is_hovering; // Style based on hover state let (bg_color, stroke_color, stroke_width) = if self.is_hovering { ( Color32::from_rgba_unmultiplied(100, 149, 237, 30), // Cornflower blue, semi-transparent Color32::from_rgb(100, 149, 237), 2.0, ) } else { ( Color32::from_gray(40), Color32::from_gray(80), 1.0, ) }; let (rect, response) = ui.allocate_exact_size(Vec2::new(available_width, height), egui::Sense::click()); // Draw background ui.painter().rect( rect, 8.0, bg_color, Stroke::new(stroke_width, stroke_color), ); // Draw dashed border effect (using multiple small rects) if !self.is_hovering { let dash_length = 10.0; let gap = 5.0; let inset = 4.0; let inner_rect = rect.shrink(inset); // Top and bottom let mut x = inner_rect.min.x; while x < inner_rect.max.x - dash_length { ui.painter().line_segment( [egui::pos2(x, inner_rect.min.y), egui::pos2(x + dash_length, inner_rect.min.y)], Stroke::new(1.0, Color32::from_gray(60)), ); ui.painter().line_segment( [egui::pos2(x, inner_rect.max.y), egui::pos2(x + dash_length, inner_rect.max.y)], Stroke::new(1.0, Color32::from_gray(60)), ); x += dash_length + gap; } // Left and right let mut y = inner_rect.min.y; while y < inner_rect.max.y - dash_length { ui.painter().line_segment( [egui::pos2(inner_rect.min.x, y), egui::pos2(inner_rect.min.x, y + dash_length)], Stroke::new(1.0, Color32::from_gray(60)), ); ui.painter().line_segment( [egui::pos2(inner_rect.max.x, y), egui::pos2(inner_rect.max.x, y + dash_length)], Stroke::new(1.0, Color32::from_gray(60)), ); y += dash_length + gap; } } // Text colors let text_color = if self.is_hovering { Color32::WHITE } else { Color32::from_gray(180) }; let font = egui::FontId::proportional(20.0); let hint_font = egui::FontId::proportional(15.0); // Calculate vertical positions for all text elements to be visually centered together // Main text line height ~24px, hint ~18px, spacing between ~12px // Total height: 24 + 24 + 12 + 18 = ~78px for non-hover, center that in rect let total_text_height = 78.0; let top_of_text = rect.center().y - total_text_height / 2.0; if self.is_hovering { // Single line when hovering - center it ui.painter().text( rect.center(), egui::Align2::CENTER_CENTER, "Drop files here", font, text_color, ); } else { // Line 1: "Drag and drop files here" ui.painter().text( egui::pos2(rect.center().x, top_of_text + 12.0), egui::Align2::CENTER_CENTER, "Drag and drop files here", font.clone(), text_color, ); // Line 2: "or click to browse" ui.painter().text( egui::pos2(rect.center().x, top_of_text + 38.0), egui::Align2::CENTER_CENTER, "or click to browse", font, text_color, ); // Hint line at bottom of text block ui.painter().text( egui::pos2(rect.center().x, top_of_text + 66.0), egui::Align2::CENTER_CENTER, "GIF, PNG, JPEG, WebP, MP4, MOV, WebM", hint_font, Color32::from_gray(140), ); } // Handle click to open file dialog if response.clicked() { let dialog = rfd::FileDialog::new() .add_filter("All Supported", &["gif", "png", "jpg", "jpeg", "webp", "mp4", "mov", "webm", "mkv", "avi", "m4v"]) .add_filter("Images", &["gif", "png", "jpg", "jpeg", "webp"]) .add_filter("Videos", &["mp4", "mov", "webm", "mkv", "avi", "m4v"]); if let Some(paths) = dialog.pick_files() { on_files_dropped(paths); } } } }