use crate::core::{Color, Point, Rect};
use crate::event::dnd::{DragPayload, DropEffect, DropTarget};
use crate::event::{Event, EventHandler};
use crate::render::RenderContext;
use crate::signal::Signal1;
use crate::widget::capability::coercion::expect_string;
use crate::widget::capability::properties_trait::{base_property_get, base_property_set};
use crate::widget::capability::types::{CapabilityAccessError, CapabilityValue};
use crate::widget::capability::WidgetProperties;
use crate::widget::{BaseWidget, Draw, Widget, WidgetKind};
use crate::{impl_widget_property_hooks, property_names_of};
pub struct DropZone {
base: BaseWidget,
accepted_type: String,
hovered: bool,
pub payload_dropped: Signal1<DragPayload>,
}
impl DropZone {
pub fn new(geometry: Rect, accepted_type: impl Into<String>) -> Self {
Self {
base: BaseWidget::new(WidgetKind::DropZone, geometry, "DropZone"),
accepted_type: accepted_type.into(),
hovered: false,
payload_dropped: Signal1::new(),
}
}
pub fn accepted_type(&self) -> &str {
&self.accepted_type
}
pub fn set_accepted_type(&mut self, accepted_type: impl Into<String>) {
self.accepted_type = accepted_type.into();
if self.hovered {
self.hovered = false;
self.base.request_redraw();
}
}
pub fn is_hovered(&self) -> bool {
self.hovered
}
pub fn clear_hover(&mut self) {
if self.hovered {
self.hovered = false;
self.base.request_redraw();
}
}
pub fn accepts(&self, payload: &DragPayload) -> bool {
payload.is_type(&self.accepted_type)
}
}
impl DropTarget for DropZone {
fn can_accept(&self, payload: &DragPayload) -> bool {
self.accepts(payload)
}
fn on_drop(&mut self, payload: &DragPayload, _pos: Point) -> DropEffect {
self.payload_dropped.emit(payload.clone());
self.hovered = false;
self.base.request_redraw();
DropEffect::Copy
}
fn preview_rect(&self, _payload: &DragPayload, _pos: Point) -> Option<Rect> {
Some(self.geometry())
}
}
impl Widget for DropZone {
fn base(&self) -> &BaseWidget {
&self.base
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn size_hint(&self) -> crate::core::Size {
crate::core::Size::new(200, 120)
}
impl_draw_bridge!();
impl_widget_property_hooks!();
}
impl WidgetProperties for DropZone {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
match name {
"accepted_type" => Ok(CapabilityValue::String(self.accepted_type().to_string())),
"hovered" => Ok(CapabilityValue::Bool(self.is_hovered())),
_ => base_property_get(self, name),
}
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
match name {
"accepted_type" => {
self.set_accepted_type(expect_string(value)?);
Ok(())
}
"hovered" => Err(CapabilityAccessError::ReadOnlyProperty),
_ => base_property_set(self, name, value),
}
}
fn property_names(&self) -> &'static [&'static str] {
property_names_of!["accepted_type", "hovered", BASE_PROPERTY_NAMES]
}
}
impl EventHandler for DropZone {
fn handle_event(&mut self, event: &Event) {
self.base.handle_event(event);
if !self.base.is_enabled() {
return;
}
match event {
Event::MouseLeave { .. } => self.clear_hover(),
_ => { }
}
}
}
impl Draw for DropZone {
fn draw(&mut self, context: &mut RenderContext) {
let rect = self.geometry();
let style = self.base.style().clone();
let theme = crate::style::resolved_theme_style("drop_zone");
let surface = style
.background_color
.or_else(|| theme.as_ref().and_then(|t| t.background_color))
.unwrap_or(Color::rgb(248, 250, 252));
let ink = style
.text_color
.or_else(|| theme.as_ref().and_then(|t| t.text_color))
.unwrap_or(Color::rgb(36, 107, 201));
let outline = style
.border_color
.or_else(|| theme.as_ref().and_then(|t| t.border_color))
.unwrap_or(Color::rgb(170, 180, 195));
let fill = if self.hovered { surface.blend(&ink, 0.12) } else { surface };
context.fill_rect(rect, fill);
let border = if self.hovered { ink } else { outline };
let dash = 8u32;
let mut x = rect.x;
while x + dash as i32 <= rect.x + rect.width as i32 {
context.draw_line_stroke(
Point::new(x, rect.y),
Point::new(x + dash as i32, rect.y),
border,
1,
);
context.draw_line_stroke(
Point::new(x, rect.y + rect.height as i32 - 1),
Point::new(x + dash as i32, rect.y + rect.height as i32 - 1),
border,
1,
);
x += (dash * 2) as i32;
}
let mut y = rect.y;
while y + dash as i32 <= rect.y + rect.height as i32 {
context.draw_line_stroke(
Point::new(rect.x, y),
Point::new(rect.x, y + dash as i32),
border,
1,
);
context.draw_line_stroke(
Point::new(rect.x + rect.width as i32 - 1, y),
Point::new(rect.x + rect.width as i32 - 1, y + dash as i32),
border,
1,
);
y += (dash * 2) as i32;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn zone() -> DropZone {
DropZone::new(Rect::new(0, 0, 200, 120), "text/plain")
}
#[test]
fn new_zone_records_its_accepted_type() {
let z = zone();
assert_eq!(z.accepted_type(), "text/plain");
assert!(!z.is_hovered());
}
#[test]
fn accepts_only_matching_type_id() {
let z = zone();
assert!(z.accepts(&DragPayload::new("text/plain", "doc1")));
assert!(!z.accepts(&DragPayload::new("image/png", "img1")));
}
#[test]
fn on_drop_emits_payload_and_clears_hover() {
let mut z = zone();
let payload = DragPayload::new("text/plain", "doc1").with_label("doc1");
let got = std::sync::Arc::new(std::sync::Mutex::new(None::<DragPayload>));
let sink = got.clone();
z.payload_dropped.connect(move |p| {
*sink.lock().unwrap() = Some(p.as_ref().clone());
});
z.hovered = true;
let effect = z.on_drop(&payload, Point::new(10, 10));
assert_eq!(effect, DropEffect::Copy);
assert!(!z.is_hovered());
let recorded = got.lock().unwrap().clone();
assert_eq!(recorded, Some(payload));
}
#[test]
fn set_accepted_type_clears_hover() {
let mut z = zone();
z.hovered = true;
z.set_accepted_type("application/json");
assert!(!z.is_hovered());
assert_eq!(z.accepted_type(), "application/json");
}
#[test]
fn preview_rect_is_the_zone_geometry() {
let z = zone();
let payload = DragPayload::new("text/plain", "doc");
assert_eq!(z.preview_rect(&payload, Point::new(0, 0)), Some(Rect::new(0, 0, 200, 120)));
}
}