diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs index 16a8a6891028..72ad27b80b79 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -67,6 +67,10 @@ pub struct CreationContext<'s> { #[cfg(feature = "glow")] pub gl: Option>, + /// The `get_proc_address` wrapper of underlying GL context + #[cfg(feature = "glow")] + pub get_proc_address: Option<&'s dyn Fn(&std::ffi::CStr) -> *const std::ffi::c_void>, + /// The underlying WGPU render state. /// /// Only available when compiling with the `wgpu` feature and using [`Renderer::Wgpu`]. diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs index 0cb7ec331f9d..8b1f16ec8078 100644 --- a/crates/eframe/src/native/glow_integration.rs +++ b/crates/eframe/src/native/glow_integration.rs @@ -284,12 +284,14 @@ impl GlowWinitApp { // Use latest raw_window_handle for eframe compatibility use raw_window_handle::{HasDisplayHandle as _, HasWindowHandle as _}; + let get_proc_address = |addr: &_| glutin.get_proc_address(addr); let window = glutin.window(ViewportId::ROOT); let cc = CreationContext { egui_ctx: integration.egui_ctx.clone(), integration_info: integration.frame.info().clone(), storage: integration.frame.storage(), gl: Some(gl), + get_proc_address: Some(&get_proc_address), #[cfg(feature = "wgpu")] wgpu_render_state: None, raw_display_handle: window.display_handle().map(|h| h.as_raw()), diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index b3451be9cb00..61bf157c007f 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -262,6 +262,8 @@ impl WgpuWinitApp { storage: integration.frame.storage(), #[cfg(feature = "glow")] gl: None, + #[cfg(feature = "glow")] + get_proc_address: None, wgpu_render_state, raw_display_handle: window.display_handle().map(|h| h.as_raw()), raw_window_handle: window.window_handle().map(|h| h.as_raw()), diff --git a/crates/eframe/src/web/app_runner.rs b/crates/eframe/src/web/app_runner.rs index 6cb331dfba93..31f6e79c01c8 100644 --- a/crates/eframe/src/web/app_runner.rs +++ b/crates/eframe/src/web/app_runner.rs @@ -76,6 +76,9 @@ impl AppRunner { #[cfg(feature = "glow")] gl: Some(painter.gl().clone()), + #[cfg(feature = "glow")] + get_proc_address: None, + #[cfg(all(feature = "wgpu", not(feature = "glow")))] wgpu_render_state: painter.render_state(), #[cfg(all(feature = "wgpu", feature = "glow"))] @@ -162,8 +165,8 @@ impl AppRunner { self.last_save_time = now_sec(); } - pub fn canvas_id(&self) -> &str { - self.painter.canvas_id() + pub fn canvas(&self) -> &web_sys::HtmlCanvasElement { + self.painter.canvas() } pub fn destroy(mut self) { @@ -179,8 +182,8 @@ impl AppRunner { /// /// The result can be painted later with a call to [`Self::run_and_paint`] or [`Self::paint`]. pub fn logic(&mut self) { - super::resize_canvas_to_screen_size(self.canvas_id(), self.web_options.max_size_points); - let canvas_size = super::canvas_size_in_points(self.canvas_id()); + super::resize_canvas_to_screen_size(self.canvas(), self.web_options.max_size_points); + let canvas_size = super::canvas_size_in_points(self.canvas()); let raw_input = self.input.new_frame(canvas_size); let full_output = self.egui_ctx.run(raw_input, |egui_ctx| { @@ -265,7 +268,7 @@ impl AppRunner { self.mutable_text_under_cursor = mutable_text_under_cursor; if self.ime != ime { - super::text_agent::move_text_cursor(ime, self.canvas_id()); + super::text_agent::move_text_cursor(ime, self.canvas()); self.ime = ime; } } diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index 71fe04e0c142..5e5f7b22622f 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -5,12 +5,12 @@ use super::*; /// Calls `request_animation_frame` to schedule repaint. /// /// It will only paint if needed, but will always call `request_animation_frame` immediately. -fn paint_and_schedule(runner_ref: &WebRunner) -> Result<(), JsValue> { +pub(crate) fn paint_and_schedule(runner_ref: &WebRunner) -> Result<(), JsValue> { // Only paint and schedule if there has been no panic if let Some(mut runner_lock) = runner_ref.try_lock() { paint_if_needed(&mut runner_lock); drop(runner_lock); - request_animation_frame(runner_ref.clone())?; + runner_ref.request_animation_frame()?; } Ok(()) } @@ -45,14 +45,6 @@ fn paint_if_needed(runner: &mut AppRunner) { runner.auto_save_if_needed(); } -pub(crate) fn request_animation_frame(runner_ref: WebRunner) -> Result<(), JsValue> { - let window = web_sys::window().unwrap(); - let closure = Closure::once(move || paint_and_schedule(&runner_ref)); - window.request_animation_frame(closure.as_ref().unchecked_ref())?; - closure.forget(); // We must forget it, or else the callback is canceled on drop - Ok(()) -} - // ------------------------------------------------------------------------ pub(crate) fn install_document_events(runner_ref: &WebRunner) -> Result<(), JsValue> { @@ -275,7 +267,7 @@ pub(crate) fn install_color_scheme_change_event(runner_ref: &WebRunner) -> Resul } pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValue> { - let canvas = canvas_element(runner_ref.try_lock().unwrap().canvas_id()).unwrap(); + let canvas = runner_ref.try_lock().unwrap().canvas().clone(); { let prevent_default_events = [ @@ -304,7 +296,7 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu let modifiers = modifiers_from_mouse_event(&event); runner.input.raw.modifiers = modifiers; if let Some(button) = button_from_mouse_event(&event) { - let pos = pos_from_mouse_event(runner.canvas_id(), &event); + let pos = pos_from_mouse_event(runner.canvas(), &event); let modifiers = runner.input.raw.modifiers; runner.input.raw.events.push(egui::Event::PointerButton { pos, @@ -331,7 +323,7 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu |event: web_sys::MouseEvent, runner| { let modifiers = modifiers_from_mouse_event(&event); runner.input.raw.modifiers = modifiers; - let pos = pos_from_mouse_event(runner.canvas_id(), &event); + let pos = pos_from_mouse_event(runner.canvas(), &event); runner.input.raw.events.push(egui::Event::PointerMoved(pos)); runner.needs_repaint.repaint_asap(); event.stop_propagation(); @@ -343,7 +335,7 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu let modifiers = modifiers_from_mouse_event(&event); runner.input.raw.modifiers = modifiers; if let Some(button) = button_from_mouse_event(&event) { - let pos = pos_from_mouse_event(runner.canvas_id(), &event); + let pos = pos_from_mouse_event(runner.canvas(), &event); let modifiers = runner.input.raw.modifiers; runner.input.raw.events.push(egui::Event::PointerButton { pos, @@ -381,7 +373,7 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu "touchstart", |event: web_sys::TouchEvent, runner| { let mut latest_touch_pos_id = runner.input.latest_touch_pos_id; - let pos = pos_from_touch_event(runner.canvas_id(), &event, &mut latest_touch_pos_id); + let pos = pos_from_touch_event(runner.canvas(), &event, &mut latest_touch_pos_id); runner.input.latest_touch_pos_id = latest_touch_pos_id; runner.input.latest_touch_pos = Some(pos); let modifiers = runner.input.raw.modifiers; @@ -404,7 +396,7 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu "touchmove", |event: web_sys::TouchEvent, runner| { let mut latest_touch_pos_id = runner.input.latest_touch_pos_id; - let pos = pos_from_touch_event(runner.canvas_id(), &event, &mut latest_touch_pos_id); + let pos = pos_from_touch_event(runner.canvas(), &event, &mut latest_touch_pos_id); runner.input.latest_touch_pos_id = latest_touch_pos_id; runner.input.latest_touch_pos = Some(pos); runner.input.raw.events.push(egui::Event::PointerMoved(pos)); @@ -467,7 +459,7 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu }); let scroll_multiplier = match unit { - egui::MouseWheelUnit::Page => canvas_size_in_points(runner.canvas_id()).y, + egui::MouseWheelUnit::Page => canvas_size_in_points(runner.canvas()).y, egui::MouseWheelUnit::Line => { #[allow(clippy::let_and_return)] let points_per_scroll_line = 8.0; // Note that this is intentionally different from what we use in winit. diff --git a/crates/eframe/src/web/input.rs b/crates/eframe/src/web/input.rs index 223693f42633..4ed0227739a1 100644 --- a/crates/eframe/src/web/input.rs +++ b/crates/eframe/src/web/input.rs @@ -1,7 +1,9 @@ -use super::{canvas_element, canvas_origin, AppRunner}; +use super::{canvas_origin, AppRunner}; -pub fn pos_from_mouse_event(canvas_id: &str, event: &web_sys::MouseEvent) -> egui::Pos2 { - let canvas = canvas_element(canvas_id).unwrap(); +pub fn pos_from_mouse_event( + canvas: &web_sys::HtmlCanvasElement, + event: &web_sys::MouseEvent, +) -> egui::Pos2 { let rect = canvas.get_bounding_client_rect(); egui::Pos2 { x: event.client_x() as f32 - rect.left() as f32, @@ -27,7 +29,7 @@ pub fn button_from_mouse_event(event: &web_sys::MouseEvent) -> Option, ) -> egui::Pos2 { @@ -47,7 +49,7 @@ pub fn pos_from_touch_event( .or_else(|| event.touches().get(0)) .map_or(Default::default(), |touch| { *touch_id_for_pos = Some(egui::TouchId::from(touch.identifier())); - pos_from_touch(canvas_origin(canvas_id), &touch) + pos_from_touch(canvas_origin(canvas), &touch) }) } @@ -59,7 +61,7 @@ fn pos_from_touch(canvas_origin: egui::Pos2, touch: &web_sys::Touch) -> egui::Po } pub fn push_touches(runner: &mut AppRunner, phase: egui::TouchPhase, event: &web_sys::TouchEvent) { - let canvas_origin = canvas_origin(runner.canvas_id()); + let canvas_origin = canvas_origin(runner.canvas()); for touch_idx in 0..event.changed_touches().length() { if let Some(touch) = event.changed_touches().item(touch_idx) { runner.input.raw.events.push(egui::Event::Touch { diff --git a/crates/eframe/src/web/mod.rs b/crates/eframe/src/web/mod.rs index cb5d6937c4c9..d88e94229fd0 100644 --- a/crates/eframe/src/web/mod.rs +++ b/crates/eframe/src/web/mod.rs @@ -100,26 +100,23 @@ fn theme_from_dark_mode(dark_mode: bool) -> Theme { } } -fn canvas_element(canvas_id: &str) -> Option { +fn get_canvas_element_by_id(canvas_id: &str) -> Option { let document = web_sys::window()?.document()?; let canvas = document.get_element_by_id(canvas_id)?; canvas.dyn_into::().ok() } -fn canvas_element_or_die(canvas_id: &str) -> web_sys::HtmlCanvasElement { - canvas_element(canvas_id) +fn get_canvas_element_by_id_or_die(canvas_id: &str) -> web_sys::HtmlCanvasElement { + get_canvas_element_by_id(canvas_id) .unwrap_or_else(|| panic!("Failed to find canvas with id {canvas_id:?}")) } -fn canvas_origin(canvas_id: &str) -> egui::Pos2 { - let rect = canvas_element(canvas_id) - .unwrap() - .get_bounding_client_rect(); +fn canvas_origin(canvas: &web_sys::HtmlCanvasElement) -> egui::Pos2 { + let rect = canvas.get_bounding_client_rect(); egui::pos2(rect.left() as f32, rect.top() as f32) } -fn canvas_size_in_points(canvas_id: &str) -> egui::Vec2 { - let canvas = canvas_element(canvas_id).unwrap(); +fn canvas_size_in_points(canvas: &web_sys::HtmlCanvasElement) -> egui::Vec2 { let pixels_per_point = native_pixels_per_point(); egui::vec2( canvas.width() as f32 / pixels_per_point, @@ -127,8 +124,10 @@ fn canvas_size_in_points(canvas_id: &str) -> egui::Vec2 { ) } -fn resize_canvas_to_screen_size(canvas_id: &str, max_size_points: egui::Vec2) -> Option<()> { - let canvas = canvas_element(canvas_id)?; +fn resize_canvas_to_screen_size( + canvas: &web_sys::HtmlCanvasElement, + max_size_points: egui::Vec2, +) -> Option<()> { let parent = canvas.parent_element()?; // Prefer the client width and height so that if the parent diff --git a/crates/eframe/src/web/text_agent.rs b/crates/eframe/src/web/text_agent.rs index 0bf8b532b7c8..a879f99d0082 100644 --- a/crates/eframe/src/web/text_agent.rs +++ b/crates/eframe/src/web/text_agent.rs @@ -5,7 +5,7 @@ use std::{cell::Cell, rc::Rc}; use wasm_bindgen::prelude::*; -use super::{canvas_element, AppRunner, WebRunner}; +use super::{AppRunner, WebRunner}; static AGENT_ID: &str = "egui_text_agent"; @@ -121,7 +121,7 @@ pub fn update_text_agent(runner: &mut AppRunner) -> Option<()> { let window = web_sys::window()?; let document = window.document()?; let input: HtmlInputElement = document.get_element_by_id(AGENT_ID)?.dyn_into().unwrap(); - let canvas_style = canvas_element(runner.canvas_id())?.style(); + let canvas_style = runner.canvas().style(); if runner.mutable_text_under_cursor { let is_already_editing = input.hidden(); @@ -205,14 +205,16 @@ fn is_mobile() -> Option { // candidate window moves following text element (agent), // so it appears that the IME candidate window moves with text cursor. // On mobile devices, there is no need to do that. -pub fn move_text_cursor(ime: Option, canvas_id: &str) -> Option<()> { +pub fn move_text_cursor( + ime: Option, + canvas: &web_sys::HtmlCanvasElement, +) -> Option<()> { let style = text_agent().style(); // Note: moving agent on mobile devices will lead to unpredictable scroll. if is_mobile() == Some(false) { ime.as_ref().and_then(|ime| { let egui::Pos2 { x, y } = ime.cursor_rect.left_top(); - let canvas = canvas_element(canvas_id)?; let bounding_rect = text_agent().get_bounding_client_rect(); let y = (y + (canvas.scroll_top() + canvas.offset_top()) as f32) .min(canvas.client_height() as f32 - bounding_rect.height() as f32); diff --git a/crates/eframe/src/web/web_painter.rs b/crates/eframe/src/web/web_painter.rs index 9c7631b90eb9..e4db8eac3161 100644 --- a/crates/eframe/src/web/web_painter.rs +++ b/crates/eframe/src/web/web_painter.rs @@ -9,8 +9,8 @@ pub(crate) trait WebPainter { // where // Self: Sized; - /// Id of the canvas in use. - fn canvas_id(&self) -> &str; + /// Reference to the canvas in use. + fn canvas(&self) -> &web_sys::HtmlCanvasElement; /// Maximum size of a texture in one direction. fn max_texture_side(&self) -> usize; diff --git a/crates/eframe/src/web/web_painter_glow.rs b/crates/eframe/src/web/web_painter_glow.rs index cd62758688f9..b54f6f64423d 100644 --- a/crates/eframe/src/web/web_painter_glow.rs +++ b/crates/eframe/src/web/web_painter_glow.rs @@ -10,7 +10,6 @@ use super::web_painter::WebPainter; pub(crate) struct WebPainterGlow { canvas: HtmlCanvasElement, - canvas_id: String, painter: egui_glow::Painter, } @@ -20,7 +19,7 @@ impl WebPainterGlow { } pub async fn new(canvas_id: &str, options: &WebOptions) -> Result { - let canvas = super::canvas_element_or_die(canvas_id); + let canvas = super::get_canvas_element_by_id_or_die(canvas_id); let (gl, shader_prefix) = init_glow_context_from_canvas(&canvas, options.webgl_context_option)?; @@ -30,11 +29,7 @@ impl WebPainterGlow { let painter = egui_glow::Painter::new(gl, shader_prefix, None) .map_err(|err| format!("Error starting glow painter: {err}"))?; - Ok(Self { - canvas, - canvas_id: canvas_id.to_owned(), - painter, - }) + Ok(Self { canvas, painter }) } } @@ -43,8 +38,8 @@ impl WebPainter for WebPainterGlow { self.painter.max_texture_side() } - fn canvas_id(&self) -> &str { - &self.canvas_id + fn canvas(&self) -> &HtmlCanvasElement { + &self.canvas } fn paint_and_update_textures( diff --git a/crates/eframe/src/web/web_painter_wgpu.rs b/crates/eframe/src/web/web_painter_wgpu.rs index 857a1f5404cd..de5ba6011115 100644 --- a/crates/eframe/src/web/web_painter_wgpu.rs +++ b/crates/eframe/src/web/web_painter_wgpu.rs @@ -41,7 +41,6 @@ impl HasDisplayHandle for EguiWebWindow { pub(crate) struct WebPainterWgpu { canvas: HtmlCanvasElement, - canvas_id: String, surface: wgpu::Surface<'static>, surface_configuration: wgpu::SurfaceConfiguration, render_state: Option, @@ -163,7 +162,7 @@ impl WebPainterWgpu { } } - let canvas = super::canvas_element_or_die(canvas_id); + let canvas = super::get_canvas_element_by_id_or_die(canvas_id); let surface = instance .create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone())) .map_err(|err| format!("failed to create wgpu surface: {err}"))?; @@ -188,7 +187,6 @@ impl WebPainterWgpu { Ok(Self { canvas, - canvas_id: canvas_id.to_owned(), render_state: Some(render_state), surface, surface_configuration, @@ -200,8 +198,8 @@ impl WebPainterWgpu { } impl WebPainter for WebPainterWgpu { - fn canvas_id(&self) -> &str { - &self.canvas_id + fn canvas(&self) -> &HtmlCanvasElement { + &self.canvas } fn max_texture_side(&self) -> usize { diff --git a/crates/eframe/src/web/web_runner.rs b/crates/eframe/src/web/web_runner.rs index 602c566e9d73..f39fc0dcace6 100644 --- a/crates/eframe/src/web/web_runner.rs +++ b/crates/eframe/src/web/web_runner.rs @@ -1,4 +1,7 @@ -use std::{cell::RefCell, rc::Rc}; +use std::{ + cell::{Cell, RefCell}, + rc::Rc, +}; use wasm_bindgen::prelude::*; @@ -24,6 +27,9 @@ pub struct WebRunner { /// They have to be in a separate `Rc` so that we don't need to pass them to /// the panic handler, since they aren't `Send`. events_to_unsubscribe: Rc>>, + + /// Used in `destroy` to cancel a pending frame. + request_animation_frame_id: Cell>, } impl WebRunner { @@ -41,6 +47,7 @@ impl WebRunner { panic_handler, runner: Rc::new(RefCell::new(None)), events_to_unsubscribe: Rc::new(RefCell::new(Default::default())), + request_animation_frame_id: Cell::new(None), } } @@ -71,7 +78,7 @@ impl WebRunner { events::install_color_scheme_change_event(self)?; } - events::request_animation_frame(self.clone())?; + self.request_animation_frame()?; } Ok(()) @@ -108,6 +115,11 @@ impl WebRunner { pub fn destroy(&self) { self.unsubscribe_from_all_events(); + if let Some(id) = self.request_animation_frame_id.get() { + let window = web_sys::window().unwrap(); + window.cancel_animation_frame(id).ok(); + } + if let Some(runner) = self.runner.replace(None) { runner.destroy(); } @@ -179,6 +191,18 @@ impl WebRunner { Ok(()) } + + pub(crate) fn request_animation_frame(&self) -> Result<(), wasm_bindgen::JsValue> { + let window = web_sys::window().unwrap(); + let closure = Closure::once({ + let runner_ref = self.clone(); + move || events::paint_and_schedule(&runner_ref) + }); + let id = window.request_animation_frame(closure.as_ref().unchecked_ref())?; + self.request_animation_frame_id.set(Some(id)); + closure.forget(); // We must forget it, or else the callback is canceled on drop + Ok(()) + } } // ---------------------------------------------------------------------------- diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index ac29a0b24047..c02fd8d275af 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -1287,7 +1287,7 @@ fn process_viewport_command( use winit::window::ResizeDirection; - log::debug!("Processing ViewportCommand::{command:?}"); + log::trace!("Processing ViewportCommand::{command:?}"); let pixels_per_point = pixels_per_point(egui_ctx, window); diff --git a/crates/egui/src/menu.rs b/crates/egui/src/menu.rs index 67689ca9d4ea..1f7a74fbf4d9 100644 --- a/crates/egui/src/menu.rs +++ b/crates/egui/src/menu.rs @@ -613,30 +613,42 @@ impl MenuState { let pointer = ui.input(|i| i.pointer.clone()); let open = self.is_open(sub_id); if self.moving_towards_current_submenu(&pointer) { + // We don't close the submenu if the pointer is on its way to hover it. // ensure to repaint once even when pointer is not moving ui.ctx().request_repaint(); } else if !open && button.hovered() { let pos = button.rect.right_top(); self.open_submenu(sub_id, pos); + } else if open + && ui.interact_bg(Sense::hover()).contains_pointer() + && !button.hovered() + && !self.hovering_current_submenu(&pointer) + { + // We are hovering something else in the menu, so close the submenu. + self.close_submenu(); } } - /// Check if `dir` points from `pos` towards left side of `rect`. - fn points_at_left_of_rect(pos: Pos2, dir: Vec2, rect: Rect) -> bool { - let vel_a = dir.angle(); - let top_a = (rect.left_top() - pos).angle(); - let bottom_a = (rect.left_bottom() - pos).angle(); - bottom_a - vel_a >= 0.0 && top_a - vel_a <= 0.0 - } - /// Check if pointer is moving towards current submenu. fn moving_towards_current_submenu(&self, pointer: &PointerState) -> bool { if pointer.is_still() { return false; } + if let Some(sub_menu) = self.current_submenu() { if let Some(pos) = pointer.hover_pos() { - return Self::points_at_left_of_rect(pos, pointer.velocity(), sub_menu.read().rect); + let rect = sub_menu.read().rect; + return rect.intesects_ray(pos, pointer.velocity().normalized()); + } + } + false + } + + /// Check if pointer is hovering current submenu. + fn hovering_current_submenu(&self, pointer: &PointerState) -> bool { + if let Some(sub_menu) = self.current_submenu() { + if let Some(pos) = pointer.hover_pos() { + return sub_menu.read().area_contains(pos); } } false @@ -673,4 +685,8 @@ impl MenuState { self.sub_menu = Some((id, Arc::new(RwLock::new(Self::new(pos))))); } } + + fn close_submenu(&mut self) { + self.sub_menu = None; + } } diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 4e28ece40f33..56db9e97f60d 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -714,8 +714,13 @@ impl Ui { .rect_contains_pointer(self.layer_id(), self.clip_rect().intersect(rect)) } - /// Is the pointer (mouse/touch) above this [`Ui`]? + /// Is the pointer (mouse/touch) above the current [`Ui`]? + /// /// Equivalent to `ui.rect_contains_pointer(ui.min_rect())` + /// + /// Note that this tests against the _current_ [`Ui::min_rect`]. + /// If you want to test against the final `min_rect`, + /// use [`Self::interact_bg`] instead. pub fn ui_contains_pointer(&self) -> bool { self.rect_contains_pointer(self.min_rect()) } diff --git a/crates/emath/src/rect.rs b/crates/emath/src/rect.rs index 4e6ee362c256..812d76c0d145 100644 --- a/crates/emath/src/rect.rs +++ b/crates/emath/src/rect.rs @@ -605,6 +605,32 @@ impl Rect { } } +impl Rect { + /// Does this Rect intersect the given ray (where `d` is normalized)? + pub fn intesects_ray(&self, o: Pos2, d: Vec2) -> bool { + let mut tmin = -f32::INFINITY; + let mut tmax = f32::INFINITY; + + if d.x != 0.0 { + let tx1 = (self.min.x - o.x) / d.x; + let tx2 = (self.max.x - o.x) / d.x; + + tmin = tmin.max(tx1.min(tx2)); + tmax = tmax.min(tx1.max(tx2)); + } + + if d.y != 0.0 { + let ty1 = (self.min.y - o.y) / d.y; + let ty2 = (self.max.y - o.y) / d.y; + + tmin = tmin.max(ty1.min(ty2)); + tmax = tmax.min(ty1.max(ty2)); + } + + tmin <= tmax + } +} + impl std::fmt::Debug for Rect { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "[{:?} - {:?}]", self.min, self.max)