Files
game_engine/crates/voltex_editor/src/widgets.rs
2026-03-26 07:32:55 +09:00

746 lines
26 KiB
Rust

use crate::ui_context::{Key, UiContext};
// Color palette
const COLOR_BG: [u8; 4] = [0x2B, 0x2B, 0x2B, 0xFF];
const COLOR_BUTTON: [u8; 4] = [0x44, 0x44, 0x55, 0xFF];
const COLOR_BUTTON_HOT: [u8; 4] = [0x55, 0x66, 0x88, 0xFF];
const COLOR_BUTTON_ACTIVE: [u8; 4] = [0x44, 0x88, 0xFF, 0xFF];
const COLOR_TEXT: [u8; 4] = [0xEE, 0xEE, 0xEE, 0xFF];
const COLOR_PANEL: [u8; 4] = [0x33, 0x33, 0x33, 0xFF];
const COLOR_SLIDER_BG: [u8; 4] = [0x44, 0x44, 0x44, 0xFF];
const COLOR_SLIDER_HANDLE: [u8; 4] = [0x88, 0x88, 0xFF, 0xFF];
const COLOR_CHECK_BG: [u8; 4] = [0x44, 0x44, 0x44, 0xFF];
const COLOR_CHECK_MARK: [u8; 4] = [0x88, 0xFF, 0x88, 0xFF];
const COLOR_INPUT_BG: [u8; 4] = [0x22, 0x22, 0x22, 0xFF];
const COLOR_INPUT_BORDER: [u8; 4] = [0x66, 0x66, 0x66, 0xFF];
const COLOR_INPUT_FOCUSED: [u8; 4] = [0x44, 0x88, 0xFF, 0xFF];
const COLOR_CURSOR: [u8; 4] = [0xFF, 0xFF, 0xFF, 0xFF];
const COLOR_SCROLLBAR_BG: [u8; 4] = [0x33, 0x33, 0x33, 0xFF];
const COLOR_SCROLLBAR_THUMB: [u8; 4]= [0x66, 0x66, 0x77, 0xFF];
const DRAG_THRESHOLD: f32 = 5.0;
impl UiContext {
/// Draw text at the current cursor position and advance to the next line.
pub fn text(&mut self, text: &str) {
let x = self.layout.cursor_x;
let y = self.layout.cursor_y;
// We need to clone font info for the borrow checker
let gw = self.font.glyph_width as f32;
let gh = self.font.glyph_height as f32;
// Draw each character
let mut cx = x;
for ch in text.chars() {
let (u0, v0, u1, v1) = self.font.glyph_uv(ch);
self.draw_list.add_rect_uv(cx, y, gw, gh, u0, v0, u1, v1, COLOR_TEXT);
cx += gw;
}
self.layout.advance_line();
}
/// Draw a button with the given label. Returns true if clicked this frame.
pub fn button(&mut self, label: &str) -> bool {
let id = self.gen_id();
let gw = self.font.glyph_width as f32;
let gh = self.font.glyph_height as f32;
let padding = self.layout.padding;
let text_w = label.len() as f32 * gw;
let btn_w = text_w + padding * 2.0;
let btn_h = gh + padding * 2.0;
let x = self.layout.cursor_x;
let y = self.layout.cursor_y;
let hovered = self.mouse_in_rect(x, y, btn_w, btn_h);
if hovered {
self.hot = Some(id);
if self.mouse_down {
self.active = Some(id);
}
}
// Determine color
let bg_color = if self.active == Some(id) && hovered {
COLOR_BUTTON_ACTIVE
} else if self.hot == Some(id) {
COLOR_BUTTON_HOT
} else {
COLOR_BUTTON
};
// Draw background rect
self.draw_list.add_rect(x, y, btn_w, btn_h, bg_color);
// Draw text centered inside button
let text_x = x + padding;
let text_y = y + padding;
let mut cx = text_x;
for ch in label.chars() {
let (u0, v0, u1, v1) = self.font.glyph_uv(ch);
self.draw_list.add_rect_uv(cx, text_y, gw, gh, u0, v0, u1, v1, COLOR_TEXT);
cx += gw;
}
self.layout.advance_line();
// Return true if mouse was released over this button while it was active
let clicked = hovered && self.mouse_released && self.active == Some(id);
if self.mouse_released {
if self.active == Some(id) {
self.active = None;
}
}
clicked
}
/// Draw a horizontal slider. Returns the (possibly new) value after interaction.
pub fn slider(&mut self, label: &str, value: f32, min: f32, max: f32) -> f32 {
let id = self.gen_id();
let gw = self.font.glyph_width as f32;
let gh = self.font.glyph_height as f32;
let padding = self.layout.padding;
let slider_w = 150.0_f32;
let slider_h = gh + padding * 2.0;
let handle_w = 10.0_f32;
let x = self.layout.cursor_x;
let y = self.layout.cursor_y;
let hovered = self.mouse_in_rect(x, y, slider_w, slider_h);
if hovered && self.mouse_clicked {
self.active = Some(id);
}
let mut new_value = value;
if self.active == Some(id) {
if self.mouse_down {
// Map mouse_x to value
let t = ((self.mouse_x - x - handle_w / 2.0) / (slider_w - handle_w)).clamp(0.0, 1.0);
new_value = min + t * (max - min);
} else if self.mouse_released {
self.active = None;
}
}
// Clamp value
new_value = new_value.clamp(min, max);
// Draw background bar
self.draw_list.add_rect(x, y, slider_w, slider_h, COLOR_SLIDER_BG);
// Draw handle
let t = if (max - min).abs() < 1e-6 {
0.0
} else {
(new_value - min) / (max - min)
};
let handle_x = x + t * (slider_w - handle_w);
self.draw_list.add_rect(handle_x, y, handle_w, slider_h, COLOR_SLIDER_HANDLE);
// Draw label to the right of the slider
let label_x = x + slider_w + padding;
let label_y = y + padding;
let mut cx = label_x;
for ch in label.chars() {
let (u0, v0, u1, v1) = self.font.glyph_uv(ch);
self.draw_list.add_rect_uv(cx, label_y, gw, gh, u0, v0, u1, v1, COLOR_TEXT);
cx += gw;
}
self.layout.advance_line();
new_value
}
/// Draw a checkbox. Returns the new checked state (toggled on click).
pub fn checkbox(&mut self, label: &str, checked: bool) -> bool {
let id = self.gen_id();
let gw = self.font.glyph_width as f32;
let gh = self.font.glyph_height as f32;
let padding = self.layout.padding;
let box_size = gh;
let x = self.layout.cursor_x;
let y = self.layout.cursor_y;
let hovered = self.mouse_in_rect(x, y, box_size + padding + label.len() as f32 * gw, gh + padding);
if hovered {
self.hot = Some(id);
}
let mut new_checked = checked;
if hovered && self.mouse_clicked {
new_checked = !checked;
}
// Draw checkbox background
self.draw_list.add_rect(x, y, box_size, box_size, COLOR_CHECK_BG);
// Draw check mark if checked
if new_checked {
let inner = 3.0;
self.draw_list.add_rect(
x + inner,
y + inner,
box_size - inner * 2.0,
box_size - inner * 2.0,
COLOR_CHECK_MARK,
);
}
// Draw label
let label_x = x + box_size + padding;
let label_y = y;
let mut cx = label_x;
for ch in label.chars() {
let (u0, v0, u1, v1) = self.font.glyph_uv(ch);
self.draw_list.add_rect_uv(cx, label_y, gw, gh, u0, v0, u1, v1, COLOR_TEXT);
cx += gw;
}
self.layout.advance_line();
new_checked
}
/// Begin a panel: draw background and title, set cursor inside panel.
pub fn begin_panel(&mut self, title: &str, x: f32, y: f32, w: f32, h: f32) {
let gh = self.font.glyph_height as f32;
let padding = self.layout.padding;
// Draw panel background
self.draw_list.add_rect(x, y, w, h, COLOR_PANEL);
// Draw title bar (slightly darker background handled by same panel color here)
let title_bar_h = gh + padding * 2.0;
self.draw_list.add_rect(x, y, w, title_bar_h, COLOR_BG);
// Draw title text
let gw = self.font.glyph_width as f32;
let mut cx = x + padding;
let ty = y + padding;
for ch in title.chars() {
let (u0, v0, u1, v1) = self.font.glyph_uv(ch);
self.draw_list.add_rect_uv(cx, ty, gw, gh, u0, v0, u1, v1, COLOR_TEXT);
cx += gw;
}
// Set cursor to inside the panel (below title bar)
self.layout = crate::layout::LayoutState::new(x + padding, y + title_bar_h + padding);
}
/// End a panel — currently a no-op; cursor remains where it was.
pub fn end_panel(&mut self) {
// Nothing for now; future could restore outer cursor state.
}
// ── Text Input Widget ─────────────────────────────────────────────
/// Draw an editable single-line text input. Returns true if the buffer changed.
///
/// `id` must be unique per text input. The widget renders a box at (x, y) with
/// the given `width`. Height is determined by the font glyph height + padding.
pub fn text_input(&mut self, id: u32, buffer: &mut String, x: f32, y: f32, width: f32) -> bool {
let gw = self.font.glyph_width as f32;
let gh = self.font.glyph_height as f32;
let padding = self.layout.padding;
let height = gh + padding * 2.0;
let hovered = self.mouse_in_rect(x, y, width, height);
// Click to focus / unfocus
if self.mouse_clicked {
if hovered {
self.focused_id = Some(id);
// Place cursor at end or at click position
let click_offset = ((self.mouse_x - x - padding) / gw).round() as usize;
self.cursor_pos = click_offset.min(buffer.len());
} else if self.focused_id == Some(id) {
self.focused_id = None;
}
}
let mut changed = false;
// Process input only if focused
if self.focused_id == Some(id) {
// Ensure cursor_pos is valid
if self.cursor_pos > buffer.len() {
self.cursor_pos = buffer.len();
}
// Process character input
let chars = self.drain_chars();
for ch in chars {
buffer.insert(self.cursor_pos, ch);
self.cursor_pos += 1;
changed = true;
}
// Process key input
let keys = self.drain_keys();
for key in keys {
match key {
Key::Backspace => {
if self.cursor_pos > 0 {
buffer.remove(self.cursor_pos - 1);
self.cursor_pos -= 1;
changed = true;
}
}
Key::Delete => {
if self.cursor_pos < buffer.len() {
buffer.remove(self.cursor_pos);
changed = true;
}
}
Key::Left => {
if self.cursor_pos > 0 {
self.cursor_pos -= 1;
}
}
Key::Right => {
if self.cursor_pos < buffer.len() {
self.cursor_pos += 1;
}
}
Key::Home => {
self.cursor_pos = 0;
}
Key::End => {
self.cursor_pos = buffer.len();
}
}
}
}
// Draw border
let border_color = if self.focused_id == Some(id) {
COLOR_INPUT_FOCUSED
} else {
COLOR_INPUT_BORDER
};
self.draw_list.add_rect(x, y, width, height, border_color);
// Draw inner background (1px border)
self.draw_list.add_rect(x + 1.0, y + 1.0, width - 2.0, height - 2.0, COLOR_INPUT_BG);
// Draw text
let text_x = x + padding;
let text_y = y + padding;
let mut cx = text_x;
for ch in buffer.chars() {
let (u0, v0, u1, v1) = self.font.glyph_uv(ch);
self.draw_list.add_rect_uv(cx, text_y, gw, gh, u0, v0, u1, v1, COLOR_TEXT);
cx += gw;
}
// Draw cursor if focused
if self.focused_id == Some(id) {
let cursor_x = text_x + self.cursor_pos as f32 * gw;
self.draw_list.add_rect(cursor_x, text_y, 1.0, gh, COLOR_CURSOR);
}
self.layout.advance_line();
changed
}
// ── Scroll Panel ──────────────────────────────────────────────────
/// Begin a scrollable panel. Content drawn between begin/end will be clipped
/// to the panel bounds. `content_height` is the total height of the content
/// inside the panel (used to compute scrollbar size).
pub fn begin_scroll_panel(&mut self, id: u32, x: f32, y: f32, w: f32, h: f32, content_height: f32) {
let scrollbar_w = 12.0_f32;
let panel_inner_w = w - scrollbar_w;
// Handle mouse wheel when hovering over the panel
let hovered = self.mouse_in_rect(x, y, w, h);
let scroll_delta = if hovered && self.mouse_scroll.abs() > 0.0 {
-self.mouse_scroll * 20.0
} else {
0.0
};
// Get or create scroll offset, apply delta and clamp
let scroll = self.scroll_offsets.entry(id).or_insert(0.0);
*scroll += scroll_delta;
let max_scroll = (content_height - h).max(0.0);
*scroll = scroll.clamp(0.0, max_scroll);
let current_scroll = *scroll;
// Draw panel background
self.draw_list.add_rect(x, y, w, h, COLOR_PANEL);
// Draw scrollbar track
let sb_x = x + panel_inner_w;
self.draw_list.add_rect(sb_x, y, scrollbar_w, h, COLOR_SCROLLBAR_BG);
// Draw scrollbar thumb
if content_height > h {
let thumb_ratio = h / content_height;
let thumb_h = (thumb_ratio * h).max(16.0);
let scroll_ratio = if max_scroll > 0.0 { current_scroll / max_scroll } else { 0.0 };
let thumb_y = y + scroll_ratio * (h - thumb_h);
self.draw_list.add_rect(sb_x, thumb_y, scrollbar_w, thumb_h, COLOR_SCROLLBAR_THUMB);
}
// Push scissor rect for content clipping
self.draw_list.push_scissor(x as u32, y as u32, panel_inner_w as u32, h as u32);
// Set cursor inside panel, offset by scroll
self.layout = crate::layout::LayoutState::new(x + self.layout.padding, y + self.layout.padding - current_scroll);
}
/// End a scrollable panel. Pops the scissor rect.
pub fn end_scroll_panel(&mut self) {
self.draw_list.pop_scissor();
}
// ── Drag and Drop ─────────────────────────────────────────────────
/// Begin dragging an item. Call this when the user presses down on a draggable element.
/// `id` identifies the source, `payload` is an arbitrary u64 value transferred on drop.
pub fn begin_drag(&mut self, id: u32, payload: u64) {
if self.mouse_clicked {
self.dragging = Some((id, payload));
self.drag_start = (self.mouse_x, self.mouse_y);
self.drag_started = false;
}
}
/// Returns true if a drag operation is currently in progress (past the threshold).
pub fn is_dragging(&self) -> bool {
if let Some(_) = self.dragging {
self.drag_started
} else {
false
}
}
/// End the current drag operation. Returns `Some((source_id, payload))` if a drag
/// was in progress and the mouse was released, otherwise `None`.
pub fn end_drag(&mut self) -> Option<(u32, u64)> {
// Update drag started state based on threshold
if let Some(_) = self.dragging {
if !self.drag_started {
let dx = self.mouse_x - self.drag_start.0;
let dy = self.mouse_y - self.drag_start.1;
if (dx * dx + dy * dy).sqrt() >= DRAG_THRESHOLD {
self.drag_started = true;
}
}
}
if self.mouse_released {
let result = if self.drag_started { self.dragging } else { None };
self.dragging = None;
self.drag_started = false;
result
} else {
None
}
}
/// Declare a drop target region. If a drag is released over this target,
/// returns the payload that was dropped. Otherwise returns `None`.
pub fn drop_target(&mut self, _id: u32, x: f32, y: f32, w: f32, h: f32) -> Option<u64> {
if self.mouse_released && self.drag_started {
if self.mouse_in_rect(x, y, w, h) {
if let Some((_src_id, payload)) = self.dragging {
return Some(payload);
}
}
}
None
}
}
#[cfg(test)]
mod tests {
use crate::ui_context::{Key, UiContext};
#[test]
fn test_button_returns_false_when_not_clicked() {
let mut ctx = UiContext::new(800.0, 600.0);
// Mouse is at (500, 500) — far from any button
ctx.begin_frame(500.0, 500.0, false);
let result = ctx.button("Click Me");
assert!(!result, "button should return false when mouse is not over it");
}
#[test]
fn test_button_returns_true_when_clicked() {
let mut ctx = UiContext::new(800.0, 600.0);
// Frame 1: mouse over button, pressed down
// Button will be at layout cursor (0, 0) with some width/height
// glyph_width=8, "OK"=2 chars, btn_w = 2*8 + 4*2 = 24, btn_h = 12 + 4*2 = 20
ctx.begin_frame(10.0, 5.0, true);
let _ = ctx.button("OK");
// Frame 2: mouse still over button, released
ctx.begin_frame(10.0, 5.0, false);
let result = ctx.button("OK");
assert!(result, "button should return true when clicked and released");
}
#[test]
fn test_slider_returns_clamped_value() {
let mut ctx = UiContext::new(800.0, 600.0);
ctx.begin_frame(0.0, 0.0, false);
// Value above max should be clamped
let v = ctx.slider("test", 150.0, 0.0, 100.0);
assert!((v - 100.0).abs() < 1e-6, "slider should clamp to max: got {}", v);
ctx.begin_frame(0.0, 0.0, false);
// Value below min should be clamped
let v2 = ctx.slider("test", -10.0, 0.0, 100.0);
assert!((v2 - 0.0).abs() < 1e-6, "slider should clamp to min: got {}", v2);
}
// ── Text Input Tests ──────────────────────────────────────────────
#[test]
fn test_text_input_basic_typing() {
let mut ctx = UiContext::new(800.0, 600.0);
let mut buf = String::new();
// Click on the text input to focus it (at x=10, y=10, width=200)
ctx.begin_frame(15.0, 15.0, true);
ctx.text_input(1, &mut buf, 10.0, 10.0, 200.0);
// Now type some characters
ctx.begin_frame(15.0, 15.0, false);
ctx.input_char('H');
ctx.input_char('i');
let changed = ctx.text_input(1, &mut buf, 10.0, 10.0, 200.0);
assert!(changed);
assert_eq!(buf, "Hi");
}
#[test]
fn test_text_input_backspace() {
let mut ctx = UiContext::new(800.0, 600.0);
let mut buf = String::from("abc");
// Focus — click far right so cursor goes to end (padding=4, gw=8, 3 chars → need x > 10+4+24=38)
ctx.begin_frame(50.0, 15.0, true);
ctx.text_input(1, &mut buf, 10.0, 10.0, 200.0);
// Backspace
ctx.begin_frame(50.0, 15.0, false);
ctx.input_key(Key::Backspace);
let changed = ctx.text_input(1, &mut buf, 10.0, 10.0, 200.0);
assert!(changed);
assert_eq!(buf, "ab");
}
#[test]
fn test_text_input_cursor_movement() {
let mut ctx = UiContext::new(800.0, 600.0);
let mut buf = String::from("abc");
// Focus — click far right so cursor goes to end
ctx.begin_frame(50.0, 15.0, true);
ctx.text_input(1, &mut buf, 10.0, 10.0, 200.0);
assert_eq!(ctx.cursor_pos, 3); // cursor at end of "abc"
// Move cursor to beginning with Home
ctx.begin_frame(15.0, 15.0, false);
ctx.input_key(Key::Home);
ctx.text_input(1, &mut buf, 10.0, 10.0, 200.0);
assert_eq!(ctx.cursor_pos, 0);
// Type 'X' at beginning
ctx.begin_frame(15.0, 15.0, false);
ctx.input_char('X');
let changed = ctx.text_input(1, &mut buf, 10.0, 10.0, 200.0);
assert!(changed);
assert_eq!(buf, "Xabc");
assert_eq!(ctx.cursor_pos, 1);
}
#[test]
fn test_text_input_delete_key() {
let mut ctx = UiContext::new(800.0, 600.0);
let mut buf = String::from("abc");
// Focus
ctx.begin_frame(15.0, 15.0, true);
ctx.text_input(1, &mut buf, 10.0, 10.0, 200.0);
// Move to Home, then Delete
ctx.begin_frame(15.0, 15.0, false);
ctx.input_key(Key::Home);
ctx.input_key(Key::Delete);
let changed = ctx.text_input(1, &mut buf, 10.0, 10.0, 200.0);
assert!(changed);
assert_eq!(buf, "bc");
}
#[test]
fn test_text_input_arrow_keys() {
let mut ctx = UiContext::new(800.0, 600.0);
let mut buf = String::from("hello");
// Focus — click far right so cursor at end
ctx.begin_frame(100.0, 15.0, true);
ctx.text_input(1, &mut buf, 10.0, 10.0, 200.0);
// Left twice from end (pos 5→3)
ctx.begin_frame(100.0, 15.0, false);
ctx.input_key(Key::Left);
ctx.input_key(Key::Left);
ctx.text_input(1, &mut buf, 10.0, 10.0, 200.0);
assert_eq!(ctx.cursor_pos, 3);
// Type 'X' at position 3
ctx.begin_frame(100.0, 15.0, false);
ctx.input_char('X');
let changed = ctx.text_input(1, &mut buf, 10.0, 10.0, 200.0);
assert!(changed);
assert_eq!(buf, "helXlo");
}
#[test]
fn test_text_input_no_change_when_not_focused() {
let mut ctx = UiContext::new(800.0, 600.0);
let mut buf = String::from("test");
// Don't click on the input — mouse at (500, 500) far away
ctx.begin_frame(500.0, 500.0, true);
ctx.input_char('X');
let changed = ctx.text_input(1, &mut buf, 10.0, 10.0, 200.0);
assert!(!changed);
assert_eq!(buf, "test");
}
// ── Scroll Panel Tests ────────────────────────────────────────────
#[test]
fn test_scroll_offset_clamping() {
let mut ctx = UiContext::new(800.0, 600.0);
// Panel at (0,0), 200x100, content_height=300
// Scroll down a lot
ctx.begin_frame(100.0, 50.0, false);
ctx.set_scroll(-100.0); // scroll down
ctx.begin_scroll_panel(1, 0.0, 0.0, 200.0, 100.0, 300.0);
ctx.end_scroll_panel();
// max_scroll = 300 - 100 = 200; scroll should be clamped
let scroll = ctx.scroll_offsets.get(&1).copied().unwrap_or(0.0);
assert!(scroll >= 0.0 && scroll <= 200.0, "scroll={}", scroll);
}
#[test]
fn test_scroll_offset_does_not_go_negative() {
let mut ctx = UiContext::new(800.0, 600.0);
// Scroll up when already at top
ctx.begin_frame(100.0, 50.0, false);
ctx.set_scroll(100.0); // scroll up
ctx.begin_scroll_panel(1, 0.0, 0.0, 200.0, 100.0, 300.0);
ctx.end_scroll_panel();
let scroll = ctx.scroll_offsets.get(&1).copied().unwrap_or(0.0);
assert!((scroll - 0.0).abs() < 1e-6, "scroll should be 0, got {}", scroll);
}
#[test]
fn test_scroll_panel_content_clipping() {
let mut ctx = UiContext::new(800.0, 600.0);
ctx.begin_frame(100.0, 50.0, false);
ctx.begin_scroll_panel(1, 10.0, 20.0, 200.0, 100.0, 300.0);
// Draw some content inside
ctx.text("Inside scroll");
// Commands drawn inside should have a scissor rect
let has_scissor = ctx.draw_list.commands.iter().any(|c| c.scissor.is_some());
assert!(has_scissor, "commands inside scroll panel should have scissor rects");
ctx.end_scroll_panel();
// Commands drawn after end_scroll_panel should NOT have scissor
let cmds_before = ctx.draw_list.commands.len();
ctx.text("Outside scroll");
let new_cmds = &ctx.draw_list.commands[cmds_before..];
let has_scissor_after = new_cmds.iter().any(|c| c.scissor.is_some());
assert!(!has_scissor_after, "commands after end_scroll_panel should not have scissor");
}
// ── Drag and Drop Tests ──────────────────────────────────────────
#[test]
fn test_drag_start_and_end() {
let mut ctx = UiContext::new(800.0, 600.0);
// Frame 1: mouse down — begin drag
ctx.begin_frame(100.0, 100.0, true);
ctx.begin_drag(1, 42);
assert!(!ctx.is_dragging(), "should not be dragging yet (below threshold)");
let _ = ctx.end_drag();
// Frame 2: mouse moved past threshold, still down
ctx.begin_frame(110.0, 100.0, true);
let _ = ctx.end_drag();
assert!(ctx.is_dragging(), "should be dragging after moving past threshold");
// Frame 3: mouse released
ctx.begin_frame(120.0, 100.0, false);
let result = ctx.end_drag();
assert!(result.is_some());
let (src_id, payload) = result.unwrap();
assert_eq!(src_id, 1);
assert_eq!(payload, 42);
}
#[test]
fn test_drop_on_target() {
let mut ctx = UiContext::new(800.0, 600.0);
// Frame 1: begin drag
ctx.begin_frame(100.0, 100.0, true);
ctx.begin_drag(1, 99);
let _ = ctx.end_drag();
// Frame 2: move past threshold
ctx.begin_frame(110.0, 100.0, true);
let _ = ctx.end_drag();
// Frame 3: release over drop target at (200, 200, 50, 50)
ctx.begin_frame(220.0, 220.0, false);
let drop_result = ctx.drop_target(2, 200.0, 200.0, 50.0, 50.0);
assert_eq!(drop_result, Some(99));
let _ = ctx.end_drag();
}
#[test]
fn test_drop_outside_target() {
let mut ctx = UiContext::new(800.0, 600.0);
// Frame 1: begin drag
ctx.begin_frame(100.0, 100.0, true);
ctx.begin_drag(1, 77);
let _ = ctx.end_drag();
// Frame 2: move past threshold
ctx.begin_frame(110.0, 100.0, true);
let _ = ctx.end_drag();
// Frame 3: release far from drop target
ctx.begin_frame(500.0, 500.0, false);
let drop_result = ctx.drop_target(2, 200.0, 200.0, 50.0, 50.0);
assert_eq!(drop_result, None);
}
}