use std::sync::atomic::{AtomicU64, Ordering};
use crate::event::drag::{DragData, DragId, DropTarget};
use crate::impl_view_meta;
use crate::layout::Rect;
use crate::style::Color;
use crate::widget::theme::{DISABLED_FG, LIGHT_GRAY};
use crate::widget::traits::{Draggable, RenderContext, View, WidgetProps, WidgetState};
use super::types::DropZoneStyle;
static DROPZONE_ID_COUNTER: AtomicU64 = AtomicU64::new(1);
pub struct DropZone<F = fn(DragData) -> bool>
where
F: FnMut(DragData) -> bool,
{
id: DragId,
placeholder: String,
accepts: Vec<&'static str>,
style: DropZoneStyle,
on_drop: Option<F>,
hovered: bool,
can_accept_current: bool,
border_color: Color,
hover_color: Color,
accept_color: Color,
reject_color: Color,
state: WidgetState,
props: WidgetProps,
min_height: u16,
}
impl DropZone<fn(DragData) -> bool> {
pub fn new(placeholder: impl Into<String>) -> Self {
let id = DROPZONE_ID_COUNTER.fetch_add(1, Ordering::Relaxed);
Self {
id,
placeholder: placeholder.into(),
accepts: Vec::new(),
style: DropZoneStyle::default(),
on_drop: None,
hovered: false,
can_accept_current: false,
border_color: DISABLED_FG,
hover_color: Color::rgb(100, 150, 255),
accept_color: Color::rgb(100, 200, 100),
reject_color: Color::rgb(200, 100, 100),
state: WidgetState::new(),
props: WidgetProps::new(),
min_height: 3,
}
}
}
impl<F> DropZone<F>
where
F: FnMut(DragData) -> bool,
{
pub fn accepts(mut self, types: &[&'static str]) -> Self {
self.accepts = types.to_vec();
self
}
pub fn accepts_all(mut self) -> Self {
self.accepts.clear();
self
}
pub fn style(mut self, style: DropZoneStyle) -> Self {
self.style = style;
self
}
pub fn border_color(mut self, color: Color) -> Self {
self.border_color = color;
self
}
pub fn hover_color(mut self, color: Color) -> Self {
self.hover_color = color;
self
}
pub fn min_height(mut self, height: u16) -> Self {
self.min_height = height;
self
}
pub fn on_drop<G>(self, handler: G) -> DropZone<G>
where
G: FnMut(DragData) -> bool,
{
DropZone {
id: self.id,
placeholder: self.placeholder,
accepts: self.accepts,
style: self.style,
on_drop: Some(handler),
hovered: self.hovered,
can_accept_current: self.can_accept_current,
border_color: self.border_color,
hover_color: self.hover_color,
accept_color: self.accept_color,
reject_color: self.reject_color,
state: self.state,
props: self.props,
min_height: self.min_height,
}
}
pub fn set_hovered(&mut self, hovered: bool, can_accept: bool) {
self.hovered = hovered;
self.can_accept_current = can_accept;
}
pub fn id(&self) -> DragId {
self.id
}
pub fn as_target(&self, bounds: Rect) -> DropTarget {
DropTarget::new(self.id, bounds).accepts(&self.accepts)
}
fn current_border_color(&self) -> Color {
if self.hovered {
if self.can_accept_current {
self.accept_color
} else {
self.reject_color
}
} else {
self.border_color
}
}
fn border_chars(&self) -> (char, char, char, char, char, char) {
match self.style {
DropZoneStyle::Solid => ('┌', '┐', '└', '┘', '─', '│'),
DropZoneStyle::Dashed => ('┌', '┐', '└', '┘', '╌', '╎'),
DropZoneStyle::Highlight | DropZoneStyle::Minimal => (' ', ' ', ' ', ' ', ' ', ' '),
}
}
}
impl<F> View for DropZone<F>
where
F: FnMut(DragData) -> bool,
{
fn render(&self, ctx: &mut RenderContext) {
let area = ctx.area;
let height = area.height.max(self.min_height);
let color = self.current_border_color();
match self.style {
DropZoneStyle::Solid | DropZoneStyle::Dashed => {
let (tl, tr, bl, br, h, v) = self.border_chars();
if let Some(cell) = ctx.get_mut(0, 0) {
cell.symbol = tl;
cell.fg = Some(color);
}
for x in 1..area.width.saturating_sub(1) {
if let Some(cell) = ctx.get_mut(x, 0) {
cell.symbol = h;
cell.fg = Some(color);
}
}
if area.width > 1 {
if let Some(cell) = ctx.get_mut(area.width - 1, 0) {
cell.symbol = tr;
cell.fg = Some(color);
}
}
let bottom_y = height.saturating_sub(1);
if let Some(cell) = ctx.get_mut(0, bottom_y) {
cell.symbol = bl;
cell.fg = Some(color);
}
for x in 1..area.width.saturating_sub(1) {
if let Some(cell) = ctx.get_mut(x, bottom_y) {
cell.symbol = h;
cell.fg = Some(color);
}
}
if area.width > 1 {
if let Some(cell) = ctx.get_mut(area.width - 1, bottom_y) {
cell.symbol = br;
cell.fg = Some(color);
}
}
for y in 1..bottom_y {
if let Some(cell) = ctx.get_mut(0, y) {
cell.symbol = v;
cell.fg = Some(color);
}
if area.width > 1 {
if let Some(cell) = ctx.get_mut(area.width - 1, y) {
cell.symbol = v;
cell.fg = Some(color);
}
}
}
}
DropZoneStyle::Highlight => {
if self.hovered {
let bg = if self.can_accept_current {
Color::rgb(30, 60, 30)
} else {
Color::rgb(60, 30, 30)
};
for y in 0..height {
for x in 0..area.width {
if let Some(cell) = ctx.get_mut(x, y) {
cell.bg = Some(bg);
}
}
}
}
}
DropZoneStyle::Minimal => {
let indicator = if self.hovered {
if self.can_accept_current {
'▶'
} else {
'✗'
}
} else {
'│'
};
for y in 0..height {
if let Some(cell) = ctx.get_mut(0, y) {
cell.symbol = indicator;
cell.fg = Some(color);
}
}
}
}
let text_y = height / 2;
let text_x: u16 = 2;
let max_len = area.width.saturating_sub(4) as usize;
let display_text = if self.hovered {
if self.can_accept_current {
"Drop here!"
} else {
"Cannot drop here"
}
} else {
&self.placeholder
};
let text_color = if self.hovered { color } else { LIGHT_GRAY };
ctx.draw_text_clipped(text_x, text_y, display_text, text_color, max_len as u16);
}
impl_view_meta!("DropZone");
}
impl DropZone<fn(DragData) -> bool> {
pub fn focused(mut self, focused: bool) -> Self {
self.state.focused = focused;
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.state.disabled = disabled;
self
}
pub fn fg(mut self, color: Color) -> Self {
self.state.fg = Some(color);
self
}
pub fn bg(mut self, color: Color) -> Self {
self.state.bg = Some(color);
self
}
pub fn is_focused(&self) -> bool {
self.state.focused
}
pub fn is_disabled(&self) -> bool {
self.state.disabled
}
pub fn set_focused(&mut self, focused: bool) {
self.state.focused = focused;
}
#[doc(hidden)]
pub fn is_hovered(&self) -> bool {
self.hovered
}
#[doc(hidden)]
pub fn can_accept_current(&self) -> bool {
self.can_accept_current
}
#[doc(hidden)]
pub fn get_min_height(&self) -> u16 {
self.min_height
}
}
impl crate::widget::traits::StyledView for DropZone<fn(DragData) -> bool> {
fn set_id(&mut self, id: impl Into<String>) {
self.props.id = Some(id.into());
}
fn add_class(&mut self, class: impl Into<String>) {
let class_str = class.into();
if !self.props.classes.contains(&class_str) {
self.props.classes.push(class_str);
}
}
fn remove_class(&mut self, class: &str) {
self.props.classes.retain(|c| c != class);
}
fn toggle_class(&mut self, class: &str) {
if self.props.classes.contains(&class.to_string()) {
self.remove_class(class);
} else {
self.add_class(class);
}
}
fn has_class(&self, class: &str) -> bool {
self.props.classes.contains(&class.to_string())
}
}
impl<F> Draggable for DropZone<F>
where
F: FnMut(DragData) -> bool,
{
fn can_drop(&self) -> bool {
true
}
fn accepted_types(&self) -> &[&'static str] {
&self.accepts
}
fn on_drag_enter(&mut self, data: &DragData) {
self.hovered = true;
self.can_accept_current = self.can_accept(data);
}
fn on_drag_leave(&mut self) {
self.hovered = false;
self.can_accept_current = false;
}
fn on_drop(&mut self, data: DragData) -> bool {
self.hovered = false;
self.can_accept_current = false;
if let Some(ref mut handler) = self.on_drop {
handler(data)
} else {
false
}
}
fn drop_bounds(&self, area: Rect) -> Rect {
area
}
}