Skip to content
Merged
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions src/sdl3/video.rs
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,55 @@ impl FlashOperation {
}
}

// Represents the result of a hit test.
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
#[repr(i32)]
pub enum HitTestResult {
Normal = sys::video::SDL_HITTEST_NORMAL.0,
Draggable = sys::video::SDL_HITTEST_DRAGGABLE.0,
ResizeTopLeft = sys::video::SDL_HITTEST_RESIZE_TOPLEFT.0,
ResizeTop = sys::video::SDL_HITTEST_RESIZE_TOP.0,
ResizeTopRight = sys::video::SDL_HITTEST_RESIZE_TOPRIGHT.0,
ResizeRight = sys::video::SDL_HITTEST_RESIZE_RIGHT.0,
ResizeBottomRight = sys::video::SDL_HITTEST_RESIZE_BOTTOMRIGHT.0,
ResizeBottom = sys::video::SDL_HITTEST_RESIZE_BOTTOM.0,
ResizeBottomLeft = sys::video::SDL_HITTEST_RESIZE_BOTTOMLEFT.0,
ResizeLeft = sys::video::SDL_HITTEST_RESIZE_LEFT.0,
}

impl HitTestResult {
pub fn from_ll(result: sys::video::SDL_HitTestResult) -> HitTestResult {
match result {
sys::video::SDL_HITTEST_NORMAL => HitTestResult::Normal,
sys::video::SDL_HITTEST_DRAGGABLE => HitTestResult::Draggable,
sys::video::SDL_HITTEST_RESIZE_TOPLEFT => HitTestResult::ResizeTopLeft,
sys::video::SDL_HITTEST_RESIZE_TOP => HitTestResult::ResizeTop,
sys::video::SDL_HITTEST_RESIZE_TOPRIGHT => HitTestResult::ResizeTopRight,
sys::video::SDL_HITTEST_RESIZE_RIGHT => HitTestResult::ResizeRight,
sys::video::SDL_HITTEST_RESIZE_BOTTOMRIGHT => HitTestResult::ResizeBottomRight,
sys::video::SDL_HITTEST_RESIZE_BOTTOM => HitTestResult::ResizeBottom,
sys::video::SDL_HITTEST_RESIZE_BOTTOMLEFT => HitTestResult::ResizeBottomLeft,
sys::video::SDL_HITTEST_RESIZE_LEFT => HitTestResult::ResizeLeft,
_ => HitTestResult::Normal,
}
}

pub fn to_ll(self) -> sys::video::SDL_HitTestResult {
match self {
HitTestResult::Normal => sys::video::SDL_HITTEST_NORMAL,
HitTestResult::Draggable => sys::video::SDL_HITTEST_DRAGGABLE,
HitTestResult::ResizeTopLeft => sys::video::SDL_HITTEST_RESIZE_TOPLEFT,
HitTestResult::ResizeTop => sys::video::SDL_HITTEST_RESIZE_TOP,
HitTestResult::ResizeTopRight => sys::video::SDL_HITTEST_RESIZE_TOPRIGHT,
HitTestResult::ResizeRight => sys::video::SDL_HITTEST_RESIZE_RIGHT,
HitTestResult::ResizeBottomRight => sys::video::SDL_HITTEST_RESIZE_BOTTOMRIGHT,
HitTestResult::ResizeBottom => sys::video::SDL_HITTEST_RESIZE_BOTTOM,
HitTestResult::ResizeBottomLeft => sys::video::SDL_HITTEST_RESIZE_BOTTOMLEFT,
HitTestResult::ResizeLeft => sys::video::SDL_HITTEST_RESIZE_LEFT,
}
}
}

/// Represents the "shell" of a `Window`.
///
/// You can set get and set many of the `SDL_Window` properties (i.e., border, size, `PixelFormat`, etc)
Expand Down Expand Up @@ -2281,6 +2330,40 @@ impl Window {
Err(get_error())
}
}

/// Sets a hit test function for the window.
#[doc(alias = "SDL_SetWindowHitTest")]
pub fn set_hit_test(
&mut self,
hit_test: impl Fn(crate::rect::Point) -> HitTestResult,
) -> Result<(), Error> {
// Box the closure to extend its lifetime and convert it to a raw pointer.
let boxed: Box<Box<dyn Fn(crate::rect::Point) -> HitTestResult>> =
Box::new(Box::new(hit_test));
Comment on lines +2346 to +2347
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is it in two boxes?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wrapped the closure twice to relax the trait bounds when dereferencing the callback from c_void pointer, as only Box'ing it once would yield

expected a `Fn(Point)` closure, found `c_void`
the trait `Fn(Point)` is not implemented for `c_void`
required for the cast from `*mut c_void` to `*mut dyn Fn(Point) -> HitTestResult

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let callback = &*(data as *const dyn Fn(Point) -> HitTestResult); might work

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried your solution but it seems the compiler still stops us from casting it into a trait pointer...

expected a `Fn(Point)` closure, found `c_void`
the trait `Fn(Point)` is not implemented for `c_void`
required for the cast from `*mut c_void` to `*const dyn Fn(Point) -> HitTestResult

Casting the void pointer into the trait pointer still requires the void pointer to implement that trait (which c_void doesn't)
I think Box is still needed to relax the trait bounds

let userdata = Box::into_raw(boxed) as *mut c_void;

unsafe extern "C" fn hit_test_sys(
_: *mut sys::video::SDL_Window,
point: *const sys::rect::SDL_Point,
data: *mut c_void,
) -> sys::video::SDL_HitTestResult {
// Reborrow the boxed closure.
let callback = data as *mut Box<dyn Fn(crate::rect::Point) -> HitTestResult>;
let point = crate::rect::Point::from_ll(*point);

(*callback)(point).to_ll()
}

unsafe {
let result =
sys::video::SDL_SetWindowHitTest(self.context.raw, Some(hit_test_sys), userdata);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like the hit_test will never be freed, we could store the userdata pointer on the Window struct and free it on Window drop perhaps.

Copy link
Contributor Author

@maik205 maik205 Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved hit_test into the Window struct in 96e8623

if result {
Ok(())
} else {
Err(get_error())
}
}
}
}

#[derive(Copy, Clone)]
Expand Down