2026-02-05 00:15:54 -05:00
|
|
|
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<F>(&mut self, ui: &mut egui::Ui, mut on_files_dropped: F)
|
|
|
|
|
where
|
|
|
|
|
F: FnMut(Vec<PathBuf>),
|
|
|
|
|
{
|
|
|
|
|
let available_width = ui.available_width();
|
2026-02-05 00:50:13 -05:00
|
|
|
let height = 140.0;
|
2026-02-05 00:15:54 -05:00
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-05 00:50:13 -05:00
|
|
|
// Text colors
|
2026-02-05 00:15:54 -05:00
|
|
|
let text_color = if self.is_hovering {
|
|
|
|
|
Color32::WHITE
|
|
|
|
|
} else {
|
|
|
|
|
Color32::from_gray(180)
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-05 00:50:13 -05:00
|
|
|
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),
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-02-05 00:15:54 -05:00
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|