scripts/DRAG_DROP_REPORT.md
Drei fehlende Kernbereiche für vollständiges HTML5-kompatibles Drag & Drop in azul:
accept + Cursor-FeedbackWenn ein draggable="true" Element in einem Browser gezogen wird:
setDragImage(): Der Entwickler kann ein eigenes Bild setzenNodeDrag Struct in core/src/drag.rs:
dom_id, node_id, start_position, current_position,
current_drop_target, drag_data
drag_offset (Click-Position relativ zum Node)gpu_state.rs behandelt nur Scrollbar-Thumb-TransformssetDragImage / drag_bitmap KonzeptDa azul kein Compositor-basiertes Geisterbild wie Browser hat, stattdessen:
Neues Feld drag_offset: LogicalPosition in NodeDrag
click_position - node_top_leftNeues Feld visual_transform: Option<ComputedTransform3D> in NodeDrag
GPU-Transform-Update in gpu_state.rs:
fn update_node_drag_transform(
gpu_cache: &mut GpuValueCache,
changes: &mut GpuEventChanges,
node_drag: &NodeDrag,
node_layout_position: LogicalPosition,
) {
let delta_x = node_drag.current_position.x
- node_drag.start_position.x;
let delta_y = node_drag.current_position.y
- node_drag.start_position.y;
let transform = ComputedTransform3D::translate(delta_x, delta_y, 0.0);
update_transform_key(gpu_cache, changes, dom_id, node_id, transform);
}
Opacity-Änderung: Gedraggter Node bekommt z.B. opacity: 0.6 via
GPU-Property-Update (nicht CSS, sondern direkte GPU-Werte)
Z-Index: Gedraggter Node wird auf höchsten Z-Index gesetzt
Bei DragEnd: Transform auf identity() zurücksetzen, Opacity
wiederherstellen
Alternativ: Bei DragStart den Node als Bitmap rendern und als Overlay zeichnen. Komplexer, aber näher am Browser-Verhalten.
Empfehlung: Option A — einfacher, nutzt bestehendes GPU-Transform-System, funktioniert für Kanban-Boards wo der User das Element direkt "anfasst".
HTML5 DnD hat ein raffiniertes Filterungssystem:
event.dataTransfer.setData("text/plain", "Hello");
event.dataTransfer.setData("application/x-kanban-task", taskId);
event.dataTransfer.effectAllowed = "move"; // copy|move|link|copyMove|...
target.addEventListener("dragover", (event) => {
// MIME-Type-Prüfung
if (event.dataTransfer.types.includes("application/x-kanban-task")) {
event.preventDefault(); // ← DAS macht es zum gültigen Drop-Target
event.dataTransfer.dropEffect = "move";
}
// KEIN preventDefault() = Drop nicht erlaubt = "Verbotszeichen"-Cursor
});
| Konzept | Verantwortlich | Wann |
|---|---|---|
dataTransfer.setData(type, data) | Quell-Node | dragstart |
dataTransfer.effectAllowed | Quell-Node | dragstart |
event.dataTransfer.types | Browser | dragenter/dragover (readonly) |
event.preventDefault() | Ziel-Node | dragover → erlaubt Drop |
event.dataTransfer.dropEffect | Ziel-Node | dragover |
event.dataTransfer.getData(type) | Ziel-Node | drop (nur hier lesbar!) |
Wichtig: Während dragover sind die Daten aus Sicherheitsgründen im
"Protected Mode" — nur die MIME-Types (.types) sind sichtbar, nicht die
eigentlichen Daten. Erst im drop-Event kann getData() aufgerufen werden.
dropEffect = "copy" → Cursor mit Plus-ZeichendropEffect = "move" → Normaler Drag-CursordropEffect = "link" → Cursor mit Link-SymboldropEffect = "none" → 🚫 Verbotszeichen-Cursor (kein Drop möglich)DragData Struct hat BTreeMap<AzString, Vec<u8>> → ✅ MIME→DatenDragEffect enum (Copy, Move, Link, All, None) → ✅ effectAllowedDropEffect enum (Copy, Move, Link, None) → ✅ dropEffectaccept-Attribut auf DOM-NodesAktuell in process_events.rs:
// Mapping (aktuell):
E::DragEnter => vec![EF::Hover(H::MouseEnter)],
E::DragOver => vec![EF::Hover(H::MouseOver)],
E::DragLeave => vec![EF::Hover(H::MouseLeave)],
E::Drop => vec![EF::Hover(H::DroppedFile)],
Problem: Diese Events werden NIE eigenständig generiert. Sie müssen in
der Event-Loop erkannt werden: Wenn ein NodeDrag aktiv ist UND die Maus
über einen neuen Node fährt, MUSS:
DragEnter auf dem neuen Node gefeuert werdenDragLeave auf dem vorherigen Node gefeuert werdenDragOver alle ~350ms auf dem aktuellen Node gefeuert werdenDrop beim Loslassen der Maus auf dem aktuellen Node gefeuert werdenNeue Methoden auf CallbackInfo:
impl CallbackInfo {
/// Verfügbare MIME-Types des aktiven Drags (im Protected Mode)
/// Nutzbar in DragEnter/DragOver/DragLeave
fn get_drag_types(&self) -> Vec<AzString>;
/// Tatsächliche Daten lesen (nur im Drop-Event!)
fn get_drag_data(&self, mime_type: &str) -> Option<Vec<u8>>;
/// effectAllowed der Quelle
fn get_drag_effect_allowed(&self) -> DragEffect;
/// dropEffect setzen (in DragOver)
fn set_drop_effect(&mut self, effect: DropEffect);
/// Drop akzeptieren (= preventDefault() in HTML)
fn accept_drop(&mut self);
}
Azul-spezifische Vereinfachung (HTML5 hat kein explizites accept-Attribut
für Drop-Zones, stattdessen passiert die Filterung im JavaScript):
// Auf DOM-Node-Ebene:
AzDom_withDropZone(dom, AzStringVec::from(&["text/plain", "application/x-task"]));
Alternativ: Kein accept-Attribut, stattdessen muss der Callback selbst
accept_drop() aufrufen (genau wie HTML5 mit preventDefault()).
Empfehlung: HTML5-Modell folgen — kein accept-Attribut, sondern:
get_drag_types() auf gewünschte MIME-Typesaccept_drop() auf wenn passendaccept_drop() ruft → automatisch dropEffect = "none"
→ Verbotszeichen-CursorIn sync_window_state oder direkt im Event-Processing:
match current_drop_effect {
DropEffect::None => set_cursor(CursorIcon::NoDrop),
DropEffect::Copy => set_cursor(CursorIcon::Copy),
DropEffect::Move => set_cursor(CursorIcon::Grabbing),
DropEffect::Link => set_cursor(CursorIcon::Alias),
}
Es gibt KEINE standardisierten CSS-Pseudo-Klassen für Drag & Drop!
Das ist ein wichtiger Punkt: Auch in echtem CSS/HTML gibt es:
:drag (war als Proposal in CSS Selectors 4, wurde entfernt):drop() (war in CSS Selectors 4 Draft, nie implementiert):drag-overStattdessen verwenden alle realen Implementierungen JavaScript-basierte Klassen-Toggle:
// DragStart: Quell-Element stylen
source.addEventListener("dragstart", (e) => {
e.target.classList.add("dragging"); // → opacity: 0.5
});
source.addEventListener("dragend", (e) => {
e.target.classList.remove("dragging");
});
// DragEnter/DragLeave: Drop-Zone stylen
target.addEventListener("dragenter", (e) => {
e.target.classList.add("drag-over"); // → border: 2px solid blue
});
target.addEventListener("dragleave", (e) => {
e.target.classList.remove("drag-over");
});
target.addEventListener("drop", (e) => {
e.target.classList.remove("drag-over");
});
Typische CSS-Klassen:
.dragging { opacity: 0.5; }
.drag-over { border: 2px solid #3b82f6; background: rgba(59,130,246,0.1); }
.drag-over.invalid { border-color: red; }
Da azul kein dynamisches classList.add() hat wie Browser, gibt es zwei Optionen:
Azul fügt eigene Pseudo-Klassen hinzu, die automatisch gesetzt werden:
| Pseudo-Klasse | Wann aktiv | Auf welchem Node |
|---|---|---|
:dragging | Zwischen DragStart und DragEnd | Quell-Node |
:drag-over | Während DragOver, wenn Drop erlaubt | Ziel-Node |
:drag-over-invalid | Während DragOver, wenn Drop NICHT erlaubt | Ziel-Node |
/* Quell-Element während Drag */
.task:dragging {
opacity: 0.4;
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
/* Gültige Drop-Zone */
.column:drag-over {
border: 2px solid #3b82f6;
background: rgba(59,130,246,0.1);
}
/* Ungültige Drop-Zone (MIME mismatch) */
.column:drag-over-invalid {
border: 2px solid #ef4444;
background: rgba(239,68,68,0.05);
}
Implementierung:
Neues PseudoStateType Variant:
pub enum PseudoStateType {
Normal, Hover, Active, Focus, Disabled, Checked,
FocusWithin, Visited, Backdrop,
Dragging, // NEU
DragOver, // NEU
DragOverInvalid, // NEU
}
In CSS-Parser (css/src/parser.rs):
"dragging" => PseudoStateType::Dragging,
"drag-over" => PseudoStateType::DragOver,
"drag-over-invalid" => PseudoStateType::DragOverInvalid,
In Pseudo-State-Berechnung (pro Frame):
NodeDrag aktiv: source_node bekommt :draggingaccept_drop() gecallt wurde → :drag-over:drag-over-invalidPseudo-State-Änderung triggert CSS-Recalc → Styles aktualisieren
sich automatisch, kein manuelles classList-Toggling nötig.
User muss im Callback selbst Klassen setzen:
AzUpdate on_drag_enter(AzRefAny data, AzCallbackInfo info) {
AzCallbackInfo_addCssClass(&info, hit_node, "drag-over");
return AzUpdate_RefreshDom;
}
Empfehlung: Option A — azul kann hier besser sein als HTML, weil die Pseudo-Klassen automatisch und ohne Boilerplate funktionieren. HTML macht es so umständlich weil CSS die Pseudo-Klassen nie standardisiert hat.
DragEnter, DragOver, DragLeave, Drop Events werden NIE generiert.
Sie sind als EventType definiert und auf Hover-Events gemappt, aber die
Event-Loop erzeugt sie nicht.
User bewegt Maus über Element X (während Drag aktiv):
1. DragEnter auf X (einmal, beim Eintreten)
2. DragOver auf X (alle ~350ms, wiederholt)
3. DragLeave von X (einmal, beim Verlassen)
Wenn User Maus loslässt über X:
4. Drop auf X (einmal, Daten lesbar)
Danach immer:
5. DragEnd auf Quell-Node (einmal, mit dropEffect-Info)
In process_events.rs, nach der Hit-Test-Berechnung:
// Pseudocode:
if let Some(node_drag) = active_node_drag {
let hovered_node = hit_test.get_deepest_node_at(cursor_pos);
// DragEnter / DragLeave
if hovered_node != node_drag.previous_hover_target {
if let Some(prev) = node_drag.previous_hover_target {
fire_event(DragLeave, prev);
}
if let Some(curr) = hovered_node {
fire_event(DragEnter, curr);
}
node_drag.previous_hover_target = hovered_node;
}
// DragOver (throttled, ~350ms)
if node_drag.last_drag_over_time.elapsed() > Duration::from_millis(350) {
if let Some(curr) = hovered_node {
fire_event(DragOver, curr);
}
node_drag.last_drag_over_time = Instant::now();
}
// Drop (auf Maus-Release)
if mouse_released {
if let Some(curr) = hovered_node {
fire_event(Drop, curr);
}
fire_event(DragEnd, source_node);
}
}
| Schritt | Aufwand | Prio | Beschreibung |
|---|---|---|---|
| 1 | Mittel | 🔴 | DragEnter/DragOver/DragLeave/Drop Events generieren — ohne diese Events funktioniert nichts |
| 2 | Klein | 🔴 | drag_offset in NodeDrag — Click-Position relativ zum Node |
| 3 | Mittel | 🔴 | GPU-Transform für gedraggten Node — translate(dx, dy) |
| 4 | Klein | 🟡 | Opacity-Änderung für gedraggten Node |
| 5 | Mittel | 🟡 | DataTransfer-API auf CallbackInfo (get_drag_types, get_drag_data, accept_drop) |
| 6 | Mittel | 🟡 | Drop-Zone-Validierung mit Cursor-Feedback (NoDrop vs. Grabbing) |
| 7 | Mittel | 🟢 | CSS Pseudo-Klassen (:dragging, :drag-over, :drag-over-invalid) |
| 8 | Klein | 🟢 | Z-Index-Override für gedraggten Node |
Gesamt: ~3-5 Tage Implementierung für vollständiges Drag & Drop.
// DragStart: MIME-Type + Daten setzen
AzUpdate on_task_drag_start(AzRefAny data, AzCallbackInfo info) {
AzCallbackInfo_setDragData(&info, "application/x-task", task_id_bytes, len);
AzCallbackInfo_setDragEffectAllowed(&info, AzDragEffect_Move);
return AzUpdate_DoNothing;
}
// DragOver auf Column: Prüfen ob Task-Type akzeptiert wird
AzUpdate on_column_drag_over(AzRefAny data, AzCallbackInfo info) {
AzStringVec types = AzCallbackInfo_getDragTypes(&info);
if (AzStringVec_contains(&types, "application/x-task")) {
AzCallbackInfo_acceptDrop(&info); // → :drag-over aktiv
AzCallbackInfo_setDropEffect(&info, AzDropEffect_Move);
}
// Wenn acceptDrop() NICHT gecallt wird:
// → :drag-over-invalid aktiv
// → Cursor = NoDrop (🚫)
return AzUpdate_DoNothing;
}
// Drop: Daten lesen + Task verschieben
AzUpdate on_column_drop(AzRefAny data, AzCallbackInfo info) {
AzOptionU8Vec task_data = AzCallbackInfo_getDragData(&info, "application/x-task");
if (!AzOptionU8Vec_isNone(&task_data)) {
// Task von alter Column entfernen, in neue einfügen
move_task(data, task_data.Some.payload);
}
return AzUpdate_RefreshDom;
}
.task:dragging {
opacity: 0.4;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.column:drag-over {
border: 2px solid #3b82f6;
background-color: rgba(59, 130, 246, 0.08);
}
.column:drag-over-invalid {
border: 2px dashed #94a3b8;
}