From b62a58c972292a9f9906e8b95b906977a5e13935 Mon Sep 17 00:00:00 2001 From: Allen Date: Mon, 4 Aug 2025 21:34:30 +0800 Subject: [PATCH 01/10] MacOS: implement drag and drop support, fix cargo clippy errors --- examples/blobs.rs | 2 +- examples/drag_drop_test.rs | 58 ++++++++++++++ examples/instancing.rs | 6 +- examples/msaa_render_texture.rs | 12 +-- examples/offscreen.rs | 10 +-- examples/post_processing.rs | 14 ++-- examples/quad.rs | 2 +- examples/triangle.rs | 2 +- examples/triangle_color4b.rs | 2 +- src/lib.rs | 1 + src/native/apple/apple_util.rs | 42 +++++------ src/native/apple/frameworks.rs | 4 +- src/native/macos.rs | 130 ++++++++++++++++++++++++++++++-- 13 files changed, 229 insertions(+), 56 deletions(-) create mode 100644 examples/drag_drop_test.rs diff --git a/examples/blobs.rs b/examples/blobs.rs index f29ea4e6a..b58ac54ab 100644 --- a/examples/blobs.rs +++ b/examples/blobs.rs @@ -47,7 +47,7 @@ impl Stage { let bindings = Bindings { vertex_buffers: vec![vertex_buffer], - index_buffer: index_buffer, + index_buffer, images: vec![], }; diff --git a/examples/drag_drop_test.rs b/examples/drag_drop_test.rs new file mode 100644 index 000000000..e16c97bf9 --- /dev/null +++ b/examples/drag_drop_test.rs @@ -0,0 +1,58 @@ +use miniquad::*; + +struct Stage { + ctx: GlContext, + dropped_files: Vec, +} + +impl EventHandler for Stage { + fn update(&mut self) {} + + fn draw(&mut self) { + // Clear the screen with a dark blue color + self.ctx.clear(Some((0.1, 0.1, 0.3, 1.0)), None, None); + + // Note: miniquad doesn't have built-in text rendering + // In a real application, you would use a text rendering library + // For now, this just clears the screen and responds to file drops + } + + fn files_dropped_event(&mut self) { + println!("Files dropped!"); + self.dropped_files.clear(); + + let count = window::dropped_file_count(); + println!("Number of dropped files: {count}"); + + for i in 0..count { + if let Some(path) = window::dropped_file_path(i) { + let path_str = path.to_string_lossy().to_string(); + println!("Dropped file {i}: {path_str}"); + self.dropped_files.push(path_str); + + if let Some(bytes) = window::dropped_file_bytes(i) { + println!(" Size: {} bytes", bytes.len()); + } + } + } + + println!("Check the console output above to see the dropped files!"); + } +} + +fn main() { + miniquad::start( + conf::Conf { + window_title: "Drag and Drop Test".to_string(), + window_width: 800, + window_height: 600, + ..Default::default() + }, + || { + Box::new(Stage { + ctx: GlContext::new(), + dropped_files: Vec::new(), + }) + }, + ); +} diff --git a/examples/instancing.rs b/examples/instancing.rs index 4084aad3f..c48b3726e 100644 --- a/examples/instancing.rs +++ b/examples/instancing.rs @@ -35,7 +35,7 @@ impl Stage { let geometry_vertex_buffer = ctx.new_buffer( BufferType::VertexBuffer, BufferUsage::Immutable, - BufferSource::slice(&vertices), + BufferSource::slice(vertices), ); #[rustfmt::skip] @@ -46,7 +46,7 @@ impl Stage { let index_buffer = ctx.new_buffer( BufferType::IndexBuffer, BufferUsage::Immutable, - BufferSource::slice(&indices), + BufferSource::slice(indices), ); // empty, dynamic instance data vertex buffer @@ -58,7 +58,7 @@ impl Stage { let bindings = Bindings { vertex_buffers: vec![geometry_vertex_buffer, positions_vertex_buffer], - index_buffer: index_buffer, + index_buffer, images: vec![], }; diff --git a/examples/msaa_render_texture.rs b/examples/msaa_render_texture.rs index d81ba16a6..d5659d6eb 100644 --- a/examples/msaa_render_texture.rs +++ b/examples/msaa_render_texture.rs @@ -84,7 +84,7 @@ impl Stage { let vertex_buffer = ctx.new_buffer( BufferType::VertexBuffer, BufferUsage::Immutable, - BufferSource::slice(&vertices), + BufferSource::slice(vertices), ); #[rustfmt::skip] @@ -100,12 +100,12 @@ impl Stage { let index_buffer = ctx.new_buffer( BufferType::IndexBuffer, BufferUsage::Immutable, - BufferSource::slice(&indices), + BufferSource::slice(indices), ); let offscreen_bind = Bindings { - vertex_buffers: vec![vertex_buffer.clone()], - index_buffer: index_buffer.clone(), + vertex_buffers: vec![vertex_buffer], + index_buffer: index_buffer, images: vec![], }; @@ -120,7 +120,7 @@ impl Stage { let vertex_buffer = ctx.new_buffer( BufferType::VertexBuffer, BufferUsage::Immutable, - BufferSource::slice(&vertices), + BufferSource::slice(vertices), ); let indices: [u16; 6] = [0, 1, 2, 0, 2, 3]; let index_buffer = ctx.new_buffer( @@ -130,7 +130,7 @@ impl Stage { ); Bindings { vertex_buffers: vec![vertex_buffer], - index_buffer: index_buffer, + index_buffer, images: vec![color_resolve_img], } }; diff --git a/examples/offscreen.rs b/examples/offscreen.rs index f8c32957a..a6b560fe6 100644 --- a/examples/offscreen.rs +++ b/examples/offscreen.rs @@ -69,7 +69,7 @@ impl Stage { let vertex_buffer = ctx.new_buffer( BufferType::VertexBuffer, BufferUsage::Immutable, - BufferSource::slice(&vertices), + BufferSource::slice(vertices), ); #[rustfmt::skip] @@ -85,18 +85,18 @@ impl Stage { let index_buffer = ctx.new_buffer( BufferType::IndexBuffer, BufferUsage::Immutable, - BufferSource::slice(&indices), + BufferSource::slice(indices), ); let offscreen_bind = Bindings { - vertex_buffers: vec![vertex_buffer.clone()], - index_buffer: index_buffer.clone(), + vertex_buffers: vec![vertex_buffer], + index_buffer: index_buffer, images: vec![], }; let display_bind = Bindings { vertex_buffers: vec![vertex_buffer], - index_buffer: index_buffer, + index_buffer, images: vec![color_img], }; diff --git a/examples/post_processing.rs b/examples/post_processing.rs index 3762d229d..f5c4d30ce 100644 --- a/examples/post_processing.rs +++ b/examples/post_processing.rs @@ -70,7 +70,7 @@ impl Stage { let vertex_buffer = ctx.new_buffer( BufferType::VertexBuffer, BufferUsage::Immutable, - BufferSource::slice(&vertices), + BufferSource::slice(vertices), ); #[rustfmt::skip] @@ -86,12 +86,12 @@ impl Stage { let index_buffer = ctx.new_buffer( BufferType::IndexBuffer, BufferUsage::Immutable, - BufferSource::slice(&indices), + BufferSource::slice(indices), ); let offscreen_bind = Bindings { - vertex_buffers: vec![vertex_buffer.clone()], - index_buffer: index_buffer.clone(), + vertex_buffers: vec![vertex_buffer], + index_buffer: index_buffer, images: vec![], }; @@ -107,7 +107,7 @@ impl Stage { let vertex_buffer = ctx.new_buffer( BufferType::VertexBuffer, BufferUsage::Immutable, - BufferSource::slice(&vertices), + BufferSource::slice(vertices), ); let indices: &[u16] = &[0, 1, 2, 0, 2, 3]; @@ -115,12 +115,12 @@ impl Stage { let index_buffer = ctx.new_buffer( BufferType::IndexBuffer, BufferUsage::Immutable, - BufferSource::slice(&indices), + BufferSource::slice(indices), ); let post_processing_bind = Bindings { vertex_buffers: vec![vertex_buffer], - index_buffer: index_buffer, + index_buffer, images: vec![color_img], }; diff --git a/examples/quad.rs b/examples/quad.rs index 821acb118..5addc8a02 100644 --- a/examples/quad.rs +++ b/examples/quad.rs @@ -53,7 +53,7 @@ impl Stage { let bindings = Bindings { vertex_buffers: vec![vertex_buffer], - index_buffer: index_buffer, + index_buffer, images: vec![texture], }; diff --git a/examples/triangle.rs b/examples/triangle.rs index 383dbf48b..d4c9234a5 100644 --- a/examples/triangle.rs +++ b/examples/triangle.rs @@ -37,7 +37,7 @@ impl Stage { let bindings = Bindings { vertex_buffers: vec![vertex_buffer], - index_buffer: index_buffer, + index_buffer, images: vec![], }; diff --git a/examples/triangle_color4b.rs b/examples/triangle_color4b.rs index e01a3c36c..b41214e7d 100644 --- a/examples/triangle_color4b.rs +++ b/examples/triangle_color4b.rs @@ -37,7 +37,7 @@ impl Stage { let bindings = Bindings { vertex_buffers: vec![vertex_buffer], - index_buffer: index_buffer, + index_buffer, images: vec![], }; diff --git a/src/lib.rs b/src/lib.rs index db1b3a7ef..73ccdf95d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -99,6 +99,7 @@ fn set_display(display: native::NativeDisplayData) { } /// This for now is Android specific since the process can continue running but the display /// is restarted. We support reinitializing the display. +#[allow(dead_code)] fn set_or_replace_display(display: native::NativeDisplayData) { if let Some(m) = NATIVE_DISPLAY.get() { // Replace existing display diff --git a/src/native/apple/apple_util.rs b/src/native/apple/apple_util.rs index 4b1973cc6..564c78bef 100644 --- a/src/native/apple/apple_util.rs +++ b/src/native/apple/apple_util.rs @@ -10,13 +10,11 @@ use crate::{ CursorIcon, }; -pub fn nsstring_to_string(string: ObjcId) -> String { - unsafe { - let utf8_string: *const core::ffi::c_uchar = msg_send![string, UTF8String]; - let utf8_len: usize = msg_send![string, lengthOfBytesUsingEncoding: UTF8_ENCODING]; - let slice = std::slice::from_raw_parts(utf8_string, utf8_len); - std::str::from_utf8_unchecked(slice).to_owned() - } +pub unsafe fn nsstring_to_string(string: ObjcId) -> String { + let utf8_string: *const core::ffi::c_uchar = msg_send![string, UTF8String]; + let utf8_len: usize = msg_send![string, lengthOfBytesUsingEncoding: UTF8_ENCODING]; + let slice = std::slice::from_raw_parts(utf8_string, utf8_len); + std::str::from_utf8_unchecked(slice).to_owned() } pub fn str_to_nsstring(str: &str) -> ObjcId { @@ -50,7 +48,7 @@ pub fn load_undocumented_cursor(cursor_name: &str) -> ObjcId { } pub unsafe fn ccfstr_from_str(inp: &str) -> CFStringRef { - let null = format!("{}\0", inp); + let null = format!("{inp}\0"); __CFStringMakeConstantString(null.as_ptr() as *const ::core::ffi::c_char) } @@ -124,23 +122,21 @@ pub fn load_webkit_cursor(cursor_name_str: &str) -> ObjcId { } } -pub fn get_event_char(event: ObjcId) -> Option { - unsafe { - let characters: ObjcId = msg_send![event, characters]; - if characters == nil { - return None; - } - let chars = nsstring_to_string(characters); +pub unsafe fn get_event_char(event: ObjcId) -> Option { + let characters: ObjcId = msg_send![event, characters]; + if characters == nil { + return None; + } + let chars = nsstring_to_string(characters); - if chars.is_empty() { - return None; - } - Some(chars.chars().next().unwrap()) + if chars.is_empty() { + return None; } + Some(chars.chars().next().unwrap()) } -pub fn get_event_key_modifier(event: ObjcId) -> KeyMods { - let flags: u64 = unsafe { msg_send![event, modifierFlags] }; +pub unsafe fn get_event_key_modifier(event: ObjcId) -> KeyMods { + let flags: u64 = msg_send![event, modifierFlags]; KeyMods { shift: flags & NSEventModifierFlags::NSShiftKeyMask as u64 != 0, ctrl: flags & NSEventModifierFlags::NSControlKeyMask as u64 != 0, @@ -149,8 +145,8 @@ pub fn get_event_key_modifier(event: ObjcId) -> KeyMods { } } -pub fn get_event_keycode(event: ObjcId) -> Option { - let scan_code: core::ffi::c_ushort = unsafe { msg_send![event, keyCode] }; +pub unsafe fn get_event_keycode(event: ObjcId) -> Option { + let scan_code: core::ffi::c_ushort = msg_send![event, keyCode]; Some(match scan_code { 0x00 => KeyCode::A, diff --git a/src/native/apple/frameworks.rs b/src/native/apple/frameworks.rs index f92b05511..8b7c31799 100644 --- a/src/native/apple/frameworks.rs +++ b/src/native/apple/frameworks.rs @@ -1233,9 +1233,9 @@ impl OSError { }) } - pub fn from_nserror(ns_error: ObjcId) -> Result<(), Self> { + pub unsafe fn from_nserror(ns_error: ObjcId) -> Result<(), Self> { if ns_error != nil { - let code: i32 = unsafe { msg_send![ns_error, code] }; + let code: i32 = msg_send![ns_error, code]; Self::from(code) } else { Ok(()) diff --git a/src/native/macos.rs b/src/native/macos.rs index 22e5abc61..2a4fd0c44 100644 --- a/src/native/macos.rs +++ b/src/native/macos.rs @@ -543,15 +543,15 @@ unsafe fn view_base_decl(decl: &mut ClassDecl) { extern "C" fn key_down(this: &Object, _sel: Sel, event: ObjcId) { let payload = get_window_payload(this); - let mods = get_event_key_modifier(event); + let mods = unsafe { get_event_key_modifier(event) }; let repeat: bool = unsafe { msg_send!(event, isARepeat) }; - if let Some(key) = get_event_keycode(event) { + if let Some(key) = unsafe { get_event_keycode(event) } { if let Some(event_handler) = payload.context() { event_handler.key_down_event(key, mods, repeat); } } - if let Some(character) = get_event_char(event) { + if let Some(character) = unsafe { get_event_char(event) } { if let Some(event_handler) = payload.context() { event_handler.char_event(character, mods, repeat); } @@ -560,8 +560,8 @@ unsafe fn view_base_decl(decl: &mut ClassDecl) { extern "C" fn key_up(this: &Object, _sel: Sel, event: ObjcId) { let payload = get_window_payload(this); - let mods = get_event_key_modifier(event); - if let Some(key) = get_event_keycode(event) { + let mods = unsafe { get_event_key_modifier(event) }; + if let Some(key) = unsafe { get_event_keycode(event) } { if let Some(event_handler) = payload.context() { event_handler.key_up_event(key, mods); } @@ -590,7 +590,7 @@ unsafe fn view_base_decl(decl: &mut ClassDecl) { } let payload = get_window_payload(this); - let mods = get_event_key_modifier(event); + let mods = unsafe { get_event_key_modifier(event) }; let flags: u64 = unsafe { msg_send![event, modifierFlags] }; let new_modifiers = Modifiers::new(flags); @@ -654,6 +654,102 @@ unsafe fn view_base_decl(decl: &mut ClassDecl) { payload.modifiers = new_modifiers; } + // Drag and Drop methods + extern "C" fn dragging_entered(_this: &Object, _sel: Sel, sender: ObjcId) -> u64 { + unsafe { + let pasteboard: ObjcId = msg_send![sender, draggingPasteboard]; + let types: ObjcId = msg_send![pasteboard, types]; + let contains_file_urls: bool = + msg_send![types, containsObject: NSPasteboardTypeFileURL]; + + if contains_file_urls { + use crate::native::apple::frameworks::NSDragOperation; + NSDragOperation::Copy as u64 + } else { + use crate::native::apple::frameworks::NSDragOperation; + NSDragOperation::None as u64 + } + } + } + + extern "C" fn dragging_updated(_this: &Object, _sel: Sel, sender: ObjcId) -> u64 { + unsafe { + let pasteboard: ObjcId = msg_send![sender, draggingPasteboard]; + let types: ObjcId = msg_send![pasteboard, types]; + let contains_file_urls: bool = + msg_send![types, containsObject: NSPasteboardTypeFileURL]; + + if contains_file_urls { + use crate::native::apple::frameworks::NSDragOperation; + NSDragOperation::Copy as u64 + } else { + use crate::native::apple::frameworks::NSDragOperation; + NSDragOperation::None as u64 + } + } + } + + extern "C" fn perform_drag_operation(this: &Object, _sel: Sel, sender: ObjcId) -> bool { + let payload = get_window_payload(this); + + unsafe { + let pasteboard: ObjcId = msg_send![sender, draggingPasteboard]; + let nsurl_class = class!(NSURL); + let classes_array: ObjcId = msg_send![class!(NSArray), arrayWithObject: nsurl_class]; + let options: ObjcId = msg_send![class!(NSDictionary), dictionary]; + let file_urls: ObjcId = msg_send![pasteboard, + readObjectsForClasses: classes_array + options: options + ]; + + if file_urls == nil { + return false; + } + + let count: usize = msg_send![file_urls, count]; + if count == 0 { + return false; + } + + // Clear previous dropped files + { + let mut d = crate::native_display().lock().unwrap(); + d.dropped_files.paths.clear(); + d.dropped_files.bytes.clear(); + } + + // Process each dropped file + for i in 0..count { + let url: ObjcId = msg_send![file_urls, objectAtIndex: i]; + if url == nil { + continue; + } + + let path_nsstring: ObjcId = msg_send![url, path]; + if path_nsstring == nil { + continue; + } + + let path_str = crate::native::apple::apple_util::nsstring_to_string(path_nsstring); + let path = std::path::PathBuf::from(path_str); + + // Try to read file contents + if let Ok(bytes) = std::fs::read(&path) { + let mut d = crate::native_display().lock().unwrap(); + d.dropped_files.paths.push(path); + d.dropped_files.bytes.push(bytes); + } + } + + // Trigger the dropped files event + if let Some(event_handler) = payload.context() { + event_handler.files_dropped_event(); + } + + true + } + } + decl.add_method( sel!(canBecomeKey), yes as extern "C" fn(&Object, Sel) -> BOOL, @@ -720,6 +816,20 @@ unsafe fn view_base_decl(decl: &mut ClassDecl) { flags_changed as extern "C" fn(&Object, Sel, ObjcId), ); decl.add_method(sel!(keyUp:), key_up as extern "C" fn(&Object, Sel, ObjcId)); + + // Add drag and drop methods + decl.add_method( + sel!(draggingEntered:), + dragging_entered as extern "C" fn(&Object, Sel, ObjcId) -> u64, + ); + decl.add_method( + sel!(draggingUpdated:), + dragging_updated as extern "C" fn(&Object, Sel, ObjcId) -> u64, + ); + decl.add_method( + sel!(performDragOperation:), + perform_drag_operation as extern "C" fn(&Object, Sel, ObjcId) -> BOOL, + ); } pub fn define_opengl_view_class() -> *const Class { @@ -813,6 +923,7 @@ pub fn define_metal_view_class() -> *const Class { decl.register() } +#[allow(clippy::mut_from_ref)] fn get_window_payload(this: &Object) -> &mut MacosDisplay { unsafe { let ptr: *mut c_void = *this.get_ivar("display_ptr"); @@ -1163,6 +1274,13 @@ where let () = msg_send![window, center]; let () = msg_send![window, setAcceptsMouseMovedEvents: YES]; + // Register the view to accept dragged file URLs + unsafe { + let file_url_type = NSPasteboardTypeFileURL; + let types_array: ObjcId = msg_send![class!(NSArray), arrayWithObject: file_url_type]; + let () = msg_send![view, registerForDraggedTypes: types_array]; + } + if conf.fullscreen { let () = msg_send![window, toggleFullScreen: nil]; } From 8873298dcbe55e38652984ce067e0865f5e0407f Mon Sep 17 00:00:00 2001 From: Allen Date: Mon, 4 Aug 2025 21:38:53 +0800 Subject: [PATCH 02/10] MacOS: fix CI build error on macos x86_64 --- src/native/macos.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/native/macos.rs b/src/native/macos.rs index 2a4fd0c44..47df600dd 100644 --- a/src/native/macos.rs +++ b/src/native/macos.rs @@ -689,7 +689,7 @@ unsafe fn view_base_decl(decl: &mut ClassDecl) { } } - extern "C" fn perform_drag_operation(this: &Object, _sel: Sel, sender: ObjcId) -> bool { + extern "C" fn perform_drag_operation(this: &Object, _sel: Sel, sender: ObjcId) -> BOOL { let payload = get_window_payload(this); unsafe { @@ -703,12 +703,12 @@ unsafe fn view_base_decl(decl: &mut ClassDecl) { ]; if file_urls == nil { - return false; + return NO; } let count: usize = msg_send![file_urls, count]; if count == 0 { - return false; + return NO; } // Clear previous dropped files @@ -746,7 +746,7 @@ unsafe fn view_base_decl(decl: &mut ClassDecl) { event_handler.files_dropped_event(); } - true + YES } } From 7dad947042a38a6c4ec718856eeee4aa1f05f1a1 Mon Sep 17 00:00:00 2001 From: Allen Date: Tue, 5 Aug 2025 20:38:06 +0800 Subject: [PATCH 03/10] MacOS: fix wrong screen_metrics --- examples/screen_metrics.rs | 44 ++++++++++++++++++++++++++++++++++++++ src/lib.rs | 21 ++++++++++++++++++ src/native/macos.rs | 8 ++++--- 3 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 examples/screen_metrics.rs diff --git a/examples/screen_metrics.rs b/examples/screen_metrics.rs new file mode 100644 index 000000000..26888a505 --- /dev/null +++ b/examples/screen_metrics.rs @@ -0,0 +1,44 @@ +use miniquad::*; + +struct Stage { + ctx: GlContext, +} + +impl EventHandler for Stage { + fn update(&mut self) {} + + fn draw(&mut self) { + self.ctx.clear(Some((0.2, 0.2, 0.2, 1.0)), None, None); + + let metrics = window::screen_metrics(); + println!("Screen Metrics:"); + println!(" Size: {}x{}", metrics.width, metrics.height); + println!(" Position: {:?}", metrics.position); + println!(" DPI Scale: {}", metrics.dpi_scale); + println!(" High DPI: {}", metrics.high_dpi); + + let (width, height) = window::screen_size(); + println!("Screen Size (legacy): {}x{}", width, height); + + println!("DPI Scale (legacy): {}", window::dpi_scale()); + println!("High DPI (legacy): {}", window::high_dpi()); + println!("---"); + } +} + +fn main() { + miniquad::start( + conf::Conf { + window_title: "Screen Metrics Demo".to_string(), + window_width: 800, + window_height: 600, + high_dpi: true, + ..Default::default() + }, + || { + Box::new(Stage { + ctx: GlContext::new(), + }) + }, + ); +} diff --git a/src/lib.rs b/src/lib.rs index 73ccdf95d..56b85b567 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -163,6 +163,18 @@ pub mod window { d.high_dpi } + /// Get comprehensive screen metrics including size, position, DPI scale, and high DPI status + pub fn screen_metrics() -> crate::ScreenMetrics { + let d = native_display().lock().unwrap(); + crate::ScreenMetrics { + width: d.screen_width as f32, + height: d.screen_height as f32, + position: d.screen_position, + dpi_scale: d.dpi_scale, + high_dpi: d.high_dpi, + } + } + pub fn blocking_event_loop() -> bool { let d = native_display().lock().unwrap(); d.blocking_event_loop @@ -352,6 +364,15 @@ pub enum CursorIcon { NWSEResize, } +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct ScreenMetrics { + pub width: f32, + pub height: f32, + pub position: (u32, u32), + pub dpi_scale: f32, + pub high_dpi: bool, +} + /// Start miniquad. pub fn start(conf: conf::Conf, f: F) where diff --git a/src/native/macos.rs b/src/native/macos.rs index 47df600dd..e6cc8f3fd 100644 --- a/src/native/macos.rs +++ b/src/native/macos.rs @@ -171,9 +171,11 @@ impl MacosDisplay { d.dpi_scale = (backing_size.width / bounds.size.width) as f32; } - let bounds: NSRect = msg_send![self.view, bounds]; - let screen_width = (bounds.size.width as f32 * d.dpi_scale) as i32; - let screen_height = (bounds.size.height as f32 * d.dpi_scale) as i32; + // Get the actual screen/monitor resolution instead of window bounds + let screen: ObjcId = msg_send![self.window, screen]; + let screen_frame: NSRect = msg_send![screen, frame]; + let screen_width = (screen_frame.size.width as f32 * d.dpi_scale) as i32; + let screen_height = (screen_frame.size.height as f32 * d.dpi_scale) as i32; let dim_changed = screen_width != d.screen_width || screen_height != d.screen_height; From 816bd9db861a9d8a2681f7a9602d97481fca938e Mon Sep 17 00:00:00 2001 From: Allen Date: Tue, 5 Aug 2025 20:52:20 +0800 Subject: [PATCH 04/10] MacOS: implement screen_metrics --- src/native/android.rs | 14 +++----------- src/native/macos.rs | 11 ++++++++--- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/native/android.rs b/src/native/android.rs index 2bd17f85f..6f6e9b2d1 100644 --- a/src/native/android.rs +++ b/src/native/android.rs @@ -187,10 +187,7 @@ impl MainThreadState { Message::SurfaceDestroyed => unsafe { self.destroy_surface(); }, - Message::SurfaceChanged { - width, - height, - } => { + Message::SurfaceChanged { width, height } => { { let mut d = crate::native_display().lock().unwrap(); d.screen_width = width as _; @@ -404,9 +401,7 @@ where // it is important to create GL context only after a first SurfaceChanged let window = 'a: loop { match rx.try_recv() { - Ok(Message::SurfaceCreated { - window, - }) => { + Ok(Message::SurfaceCreated { window }) => { break 'a window; } _ => {} @@ -414,10 +409,7 @@ where }; let (screen_width, screen_height) = 'a: loop { match rx.try_recv() { - Ok(Message::SurfaceChanged { - width, - height, - }) => { + Ok(Message::SurfaceChanged { width, height }) => { break 'a (width as f32, height as f32); } _ => {} diff --git a/src/native/macos.rs b/src/native/macos.rs index e6cc8f3fd..b197a0d3d 100644 --- a/src/native/macos.rs +++ b/src/native/macos.rs @@ -177,10 +177,16 @@ impl MacosDisplay { let screen_width = (screen_frame.size.width as f32 * d.dpi_scale) as i32; let screen_height = (screen_frame.size.height as f32 * d.dpi_scale) as i32; + // Get window position + let window_frame: NSRect = msg_send![self.window, frame]; + let window_x = (window_frame.origin.x * d.dpi_scale as f64) as u32; + let window_y = (window_frame.origin.y * d.dpi_scale as f64) as u32; + let dim_changed = screen_width != d.screen_width || screen_height != d.screen_height; d.screen_width = screen_width; d.screen_height = screen_height; + d.screen_position = (window_x, window_y); if dim_changed { Some((screen_width, screen_height)) @@ -326,10 +332,9 @@ pub fn define_cocoa_window_delegate() -> *const Class { // Startup: the gl_context has not yet been created. return; } - if let Some(event_handler) = payload.context() { - event_handler.window_minimized_event(); - } + // Update screen position when window moves unsafe { + payload.update_dimensions(); msg_send_![payload.gl_context, update]; } } From aab676c3ad5a1b206e4e8340b6f4a33f0c597650 Mon Sep 17 00:00:00 2001 From: Allen Date: Tue, 5 Aug 2025 21:00:22 +0800 Subject: [PATCH 05/10] MacOS: implement set_window_position --- src/native/macos.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/native/macos.rs b/src/native/macos.rs index b197a0d3d..a6b70fb2d 100644 --- a/src/native/macos.rs +++ b/src/native/macos.rs @@ -97,6 +97,12 @@ impl MacosDisplay { }; let () = unsafe { msg_send![self.window, setFrame:frame display:true animate:true] }; } + fn set_window_position(&mut self, new_x: u32, new_y: u32) { + let mut frame: NSRect = unsafe { msg_send![self.window, frame] }; + frame.origin.x = new_x as f64; + frame.origin.y = new_y as f64; + let () = unsafe { msg_send![self.window, setFrame:frame display:true animate:true] }; + } fn set_fullscreen(&mut self, fullscreen: bool) { if self.fullscreen != fullscreen { self.fullscreen = fullscreen; @@ -209,9 +215,7 @@ impl MacosDisplay { new_height, } => self.set_window_size(new_width as _, new_height as _), SetFullscreen(fullscreen) => self.set_fullscreen(fullscreen), - SetWindowPosition { .. } => { - eprintln!("Not implemented for macos"); - } + SetWindowPosition { new_x, new_y } => self.set_window_position(new_x, new_y), _ => {} } } From 1c8c912130a07b21483724773d9f921617909a4a Mon Sep 17 00:00:00 2001 From: Allen Date: Tue, 5 Aug 2025 21:25:21 +0800 Subject: [PATCH 06/10] Fix screen_metrics and screen_position to return window info, and add monitor info related APIs --- examples/monitor_metrics.rs | 69 +++++++++++++++++++ src/lib.rs | 25 +++++++ src/native.rs | 70 ++++++++++++++++++++ src/native/android.rs | 13 ++++ src/native/ios.rs | 30 +++++++-- src/native/linux_wayland.rs | 26 ++++++++ src/native/linux_x11.rs | 51 ++++++++++++++ src/native/macos.rs | 114 +++++++++++++++++++++++++++++--- src/native/wasm.rs | 13 ++++ src/native/windows.rs | 128 ++++++++++++++++++++++++++++++++++++ 10 files changed, 524 insertions(+), 15 deletions(-) create mode 100644 examples/monitor_metrics.rs diff --git a/examples/monitor_metrics.rs b/examples/monitor_metrics.rs new file mode 100644 index 000000000..6ae4c6cf4 --- /dev/null +++ b/examples/monitor_metrics.rs @@ -0,0 +1,69 @@ +use miniquad::*; + +struct Stage { + ctx: GlContext, +} + +impl EventHandler for Stage { + fn update(&mut self) {} + + fn draw(&mut self) { + self.ctx.clear(Some((0.0, 0.0, 0.0, 1.0)), None, None); + + // Print monitor information every 60 frames (approximately once per second at 60fps) + static mut FRAME_COUNT: u32 = 0; + unsafe { + FRAME_COUNT += 1; + if FRAME_COUNT % 60 == 0 { + println!("=== Monitor Information ==="); + + // Primary monitor + let primary = window::primary_monitor(); + println!("Primary Monitor:"); + println!(" Name: {:?}", primary.name); + println!(" Size: {}x{}", primary.width, primary.height); + println!(" Position: {:?}", primary.position); + println!(" DPI Scale: {}", primary.dpi_scale); + println!(" Refresh Rate: {:?}", primary.refresh_rate); + + // Current monitor (where the window is displayed) + let current = window::current_monitor(); + println!("\nCurrent Monitor (where window is displayed):"); + println!(" Name: {:?}", current.name); + println!(" Size: {}x{}", current.width, current.height); + println!(" Position: {:?}", current.position); + println!(" DPI Scale: {}", current.dpi_scale); + println!(" Refresh Rate: {:?}", current.refresh_rate); + + // All monitors + let monitors = window::monitors(); + println!("\nAll Monitors ({} total):", monitors.len()); + for (i, monitor) in monitors.iter().enumerate() { + println!(" Monitor {}:", i + 1); + println!(" Name: {:?}", monitor.name); + println!(" Size: {}x{}", monitor.width, monitor.height); + println!(" Position: {:?}", monitor.position); + println!(" DPI Scale: {}", monitor.dpi_scale); + println!(" Refresh Rate: {:?}", monitor.refresh_rate); + } + + // Compare with window metrics + let window_metrics = window::screen_metrics(); + println!("\nWindow Metrics (for comparison):"); + println!(" Size: {}x{}", window_metrics.width, window_metrics.height); + println!(" Position: {:?}", window_metrics.position); + println!(" DPI Scale: {}", window_metrics.dpi_scale); + println!(" High DPI: {}", window_metrics.high_dpi); + println!("---"); + } + } + } +} + +fn main() { + miniquad::start(conf::Conf::default(), || { + Box::new(Stage { + ctx: GlContext::new(), + }) + }); +} diff --git a/src/lib.rs b/src/lib.rs index 56b85b567..7c8cc97da 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -175,6 +175,21 @@ pub mod window { } } + /// Get metrics for the primary monitor + pub fn primary_monitor() -> crate::MonitorMetrics { + native::primary_monitor_impl() + } + + /// Get metrics for all available monitors + pub fn monitors() -> Vec { + native::monitors_impl() + } + + /// Get metrics for the monitor where the current window is displayed + pub fn current_monitor() -> crate::MonitorMetrics { + native::current_monitor_impl() + } + pub fn blocking_event_loop() -> bool { let d = native_display().lock().unwrap(); d.blocking_event_loop @@ -373,6 +388,16 @@ pub struct ScreenMetrics { pub high_dpi: bool, } +#[derive(Debug, Clone, PartialEq)] +pub struct MonitorMetrics { + pub width: f32, + pub height: f32, + pub position: (u32, u32), + pub dpi_scale: f32, + pub refresh_rate: Option, + pub name: Option, +} + /// Start miniquad. pub fn start(conf: conf::Conf, f: F) where diff --git a/src/native.rs b/src/native.rs index 88640a55c..e110b6345 100644 --- a/src/native.rs +++ b/src/native.rs @@ -119,3 +119,73 @@ pub mod gl; pub use wasm::webgl as gl; pub mod query_stab; + +// Monitor metrics implementation functions +pub fn primary_monitor_impl() -> crate::MonitorMetrics { + #[cfg(target_os = "macos")] + return macos::primary_monitor(); + #[cfg(target_os = "windows")] + return windows::primary_monitor(); + #[cfg(target_os = "linux")] + return linux_monitor(); + #[cfg(target_os = "ios")] + return ios::primary_monitor(); + #[cfg(target_os = "android")] + return android::primary_monitor(); + #[cfg(target_arch = "wasm32")] + return wasm::primary_monitor(); +} + +pub fn monitors_impl() -> Vec { + #[cfg(target_os = "macos")] + return macos::monitors(); + #[cfg(target_os = "windows")] + return windows::monitors(); + #[cfg(target_os = "linux")] + return linux_monitors(); + #[cfg(target_os = "ios")] + return vec![ios::primary_monitor()]; + #[cfg(target_os = "android")] + return vec![android::primary_monitor()]; + #[cfg(target_arch = "wasm32")] + return vec![wasm::primary_monitor()]; +} + +pub fn current_monitor_impl() -> crate::MonitorMetrics { + #[cfg(target_os = "macos")] + return macos::current_monitor(); + #[cfg(target_os = "windows")] + return windows::current_monitor(); + #[cfg(target_os = "linux")] + return linux_current_monitor(); + #[cfg(target_os = "ios")] + return ios::primary_monitor(); + #[cfg(target_os = "android")] + return android::primary_monitor(); + #[cfg(target_arch = "wasm32")] + return wasm::primary_monitor(); +} + +#[cfg(target_os = "linux")] +fn linux_monitor() -> crate::MonitorMetrics { + #[cfg(feature = "wayland")] + return linux_wayland::primary_monitor(); + #[cfg(not(feature = "wayland"))] + return linux_x11::primary_monitor(); +} + +#[cfg(target_os = "linux")] +fn linux_monitors() -> Vec { + #[cfg(feature = "wayland")] + return linux_wayland::monitors(); + #[cfg(not(feature = "wayland"))] + return linux_x11::monitors(); +} + +#[cfg(target_os = "linux")] +fn linux_current_monitor() -> crate::MonitorMetrics { + #[cfg(feature = "wayland")] + return linux_wayland::current_monitor(); + #[cfg(not(feature = "wayland"))] + return linux_x11::current_monitor(); +} diff --git a/src/native/android.rs b/src/native/android.rs index 6f6e9b2d1..6c3636288 100644 --- a/src/native/android.rs +++ b/src/native/android.rs @@ -698,3 +698,16 @@ pub(crate) unsafe fn load_asset(filepath: *const ::core::ffi::c_char, out: *mut (*out).content = buffer as _; } } + +pub fn primary_monitor() -> crate::MonitorMetrics { + // On Android, get the screen metrics from the current display + let d = crate::native_display().lock().unwrap(); + crate::MonitorMetrics { + width: d.screen_width as f32, + height: d.screen_height as f32, + position: (0, 0), // Android is single screen, fullscreen + dpi_scale: d.dpi_scale, + refresh_rate: None, // Android doesn't easily expose refresh rate + name: Some("Android Screen".to_string()), + } +} diff --git a/src/native/ios.rs b/src/native/ios.rs index a98831eda..028d01f24 100644 --- a/src/native/ios.rs +++ b/src/native/ios.rs @@ -312,20 +312,20 @@ pub fn define_glk_or_mtk_view_dlg(superclass: &Class) -> *const Class { payload.init_event_handler(); } - let main_screen: ObjcId = unsafe { msg_send![class!(UIScreen), mainScreen] }; - let screen_rect: NSRect = unsafe { msg_send![main_screen, bounds] }; + // Get the view bounds for the drawable area instead of screen bounds + let view_bounds: NSRect = unsafe { msg_send![payload.view, bounds] }; let high_dpi = native_display().lock().unwrap().high_dpi; let (screen_width, screen_height) = if high_dpi { ( - screen_rect.size.width as i32 * 2, - screen_rect.size.height as i32 * 2, + view_bounds.size.width as i32 * 2, + view_bounds.size.height as i32 * 2, ) } else { let content_scale_factor: f64 = unsafe { msg_send![payload.view, contentScaleFactor] }; ( - (screen_rect.size.width * content_scale_factor) as i32, - (screen_rect.size.height * content_scale_factor) as i32, + (view_bounds.size.width * content_scale_factor) as i32, + (view_bounds.size.height * content_scale_factor) as i32, ) }; @@ -843,3 +843,21 @@ where UIApplicationMain(argc, &mut argv, nil, class_string); } + +pub fn primary_monitor() -> crate::MonitorMetrics { + use crate::native::apple::{apple_util::*, frameworks::*}; + unsafe { + let main_screen: ObjcId = msg_send![class!(UIScreen), mainScreen]; + let screen_bounds: NSRect = msg_send![main_screen, bounds]; + let scale: f64 = msg_send![main_screen, scale]; + + crate::MonitorMetrics { + width: screen_bounds.size.width as f32, + height: screen_bounds.size.height as f32, + position: (0, 0), // iOS is single screen, fullscreen + dpi_scale: scale as f32, + refresh_rate: None, // iOS doesn't expose refresh rate easily + name: Some("iOS Screen".to_string()), + } + } +} diff --git a/src/native/linux_wayland.rs b/src/native/linux_wayland.rs index 3b8ff0b46..cb623803f 100644 --- a/src/native/linux_wayland.rs +++ b/src/native/linux_wayland.rs @@ -1311,3 +1311,29 @@ where Some(()) } + +pub fn primary_monitor() -> crate::MonitorMetrics { + // For Wayland, getting monitor information requires a complex approach + // involving wl_output protocol. For simplicity, we'll return default values. + // A full implementation would need to track wl_output events during setup. + crate::MonitorMetrics { + width: 1920.0, + height: 1080.0, + position: (0, 0), + dpi_scale: 1.0, + refresh_rate: None, + name: Some("Wayland Monitor".to_string()), + } +} + +pub fn monitors() -> Vec { + // Wayland monitor enumeration requires tracking wl_output protocol events + // For basic implementation, return just primary monitor + vec![primary_monitor()] +} + +pub fn current_monitor() -> crate::MonitorMetrics { + // For Wayland, return the primary monitor + // A proper implementation would track which monitor the surface is on + primary_monitor() +} diff --git a/src/native/linux_x11.rs b/src/native/linux_x11.rs index 31c9605bd..9c4ce50b2 100644 --- a/src/native/linux_x11.rs +++ b/src/native/linux_x11.rs @@ -701,3 +701,54 @@ where } Ok(()) } + +pub fn primary_monitor() -> crate::MonitorMetrics { + unsafe { + let display = (LIBX11.XOpenDisplay)(std::ptr::null()); + if display.is_null() { + return crate::MonitorMetrics { + width: 1920.0, + height: 1080.0, + position: (0, 0), + dpi_scale: 1.0, + refresh_rate: None, + name: Some("Primary Monitor".to_string()), + }; + } + + let screen = (LIBX11.XDefaultScreen)(display); + let width = (LIBX11.XDisplayWidth)(display, screen) as f32; + let height = (LIBX11.XDisplayHeight)(display, screen) as f32; + + // Get DPI information + let width_mm = (LIBX11.XDisplayWidthMM)(display, screen) as f32; + let dpi_scale = if width_mm > 0.0 { + (width * 25.4 / width_mm) / 96.0 // Convert to DPI scale factor + } else { + 1.0 + }; + + (LIBX11.XCloseDisplay)(display); + + crate::MonitorMetrics { + width, + height, + position: (0, 0), + dpi_scale, + refresh_rate: None, + name: Some("X11 Primary Monitor".to_string()), + } + } +} + +pub fn monitors() -> Vec { + // For basic X11 implementation, return just the primary monitor + // A full implementation would use XRandR extension to enumerate all monitors + vec![primary_monitor()] +} + +pub fn current_monitor() -> crate::MonitorMetrics { + // For basic X11 implementation, return the primary monitor + // A proper implementation would determine which monitor contains the window + primary_monitor() +} diff --git a/src/native/macos.rs b/src/native/macos.rs index a6b70fb2d..88cfc74bb 100644 --- a/src/native/macos.rs +++ b/src/native/macos.rs @@ -177,16 +177,14 @@ impl MacosDisplay { d.dpi_scale = (backing_size.width / bounds.size.width) as f32; } - // Get the actual screen/monitor resolution instead of window bounds - let screen: ObjcId = msg_send![self.window, screen]; - let screen_frame: NSRect = msg_send![screen, frame]; - let screen_width = (screen_frame.size.width as f32 * d.dpi_scale) as i32; - let screen_height = (screen_frame.size.height as f32 * d.dpi_scale) as i32; + // Get the window content view bounds for the drawable area + let bounds: NSRect = msg_send![self.view, bounds]; + let screen_width = (bounds.size.width as f32 * d.dpi_scale) as i32; + let screen_height = (bounds.size.height as f32 * d.dpi_scale) as i32; - // Get window position - let window_frame: NSRect = msg_send![self.window, frame]; - let window_x = (window_frame.origin.x * d.dpi_scale as f64) as u32; - let window_y = (window_frame.origin.y * d.dpi_scale as f64) as u32; + // For window metrics, position is relative to the window itself (0,0) + let window_x = 0u32; + let window_y = 0u32; let dim_changed = screen_width != d.screen_width || screen_height != d.screen_height; @@ -1353,3 +1351,101 @@ where } } } + +pub fn primary_monitor() -> crate::MonitorMetrics { + use crate::native::apple::{apple_util::*, frameworks::*}; + unsafe { + let main_screen: ObjcId = msg_send![class!(NSScreen), mainScreen]; + let screen_frame: NSRect = msg_send![main_screen, frame]; + let backing_scale: f64 = msg_send![main_screen, backingScaleFactor]; + let screen_name: ObjcId = msg_send![main_screen, localizedName]; + let name_str = if screen_name != nil { + Some(nsstring_to_string(screen_name)) + } else { + None + }; + + crate::MonitorMetrics { + width: screen_frame.size.width as f32, + height: screen_frame.size.height as f32, + position: (screen_frame.origin.x as u32, screen_frame.origin.y as u32), + dpi_scale: backing_scale as f32, + refresh_rate: None, // NSScreen doesn't easily expose refresh rate + name: name_str, + } + } +} + +pub fn monitors() -> Vec { + use crate::native::apple::{apple_util::*, frameworks::*}; + unsafe { + let screens: ObjcId = msg_send![class!(NSScreen), screens]; + let count: usize = msg_send![screens, count]; + let mut monitors = Vec::new(); + + for i in 0..count { + let screen: ObjcId = msg_send![screens, objectAtIndex: i]; + let screen_frame: NSRect = msg_send![screen, frame]; + let backing_scale: f64 = msg_send![screen, backingScaleFactor]; + let screen_name: ObjcId = msg_send![screen, localizedName]; + let name_str = if screen_name != nil { + Some(nsstring_to_string(screen_name)) + } else { + None + }; + + monitors.push(crate::MonitorMetrics { + width: screen_frame.size.width as f32, + height: screen_frame.size.height as f32, + position: (screen_frame.origin.x as u32, screen_frame.origin.y as u32), + dpi_scale: backing_scale as f32, + refresh_rate: None, + name: name_str, + }); + } + + monitors + } +} + +pub fn current_monitor() -> crate::MonitorMetrics { + use crate::native::apple::{apple_util::*, frameworks::*}; + + // Get the current window's screen from the view + let d = crate::native_display().lock().unwrap(); + let view = d.view; + drop(d); // Release lock early + + unsafe { + // Get the window from the view + let window: ObjcId = msg_send![view, window]; + if window == nil { + // Fallback to primary monitor if no window + return primary_monitor(); + } + + // Get the screen that contains this window + let screen: ObjcId = msg_send![window, screen]; + if screen == nil { + return primary_monitor(); + } + + let screen_frame: NSRect = msg_send![screen, frame]; + let backing_scale: f64 = msg_send![screen, backingScaleFactor]; + let screen_name: ObjcId = msg_send![screen, localizedName]; + let name_str = if screen_name != nil { + Some(nsstring_to_string(screen_name)) + } else { + None + }; + + crate::MonitorMetrics { + width: screen_frame.size.width as f32, + height: screen_frame.size.height as f32, + position: (screen_frame.origin.x as u32, screen_frame.origin.y as u32), + dpi_scale: backing_scale as f32, + refresh_rate: None, + name: name_str, + } + } +} diff --git a/src/native/wasm.rs b/src/native/wasm.rs index 6ac297e53..a5c87d61a 100644 --- a/src/native/wasm.rs +++ b/src/native/wasm.rs @@ -365,3 +365,16 @@ pub extern "C" fn on_file_dropped( d.dropped_files.paths.push(path); d.dropped_files.bytes.push(bytes); } + +pub fn primary_monitor() -> crate::MonitorMetrics { + // For WASM, get the browser window/screen dimensions + // This is a basic implementation - could be enhanced with JS interop + crate::MonitorMetrics { + width: 1920.0, // Default fallback + height: 1080.0, + position: (0, 0), + dpi_scale: 1.0, + refresh_rate: None, + name: Some("Browser".to_string()), + } +} diff --git a/src/native/windows.rs b/src/native/windows.rs index 28bf447cb..5e180a3d1 100644 --- a/src/native/windows.rs +++ b/src/native/windows.rs @@ -1008,3 +1008,131 @@ where DestroyWindow(wnd); } } + +pub fn primary_monitor() -> crate::MonitorMetrics { + use winapi::um::winuser::*; + unsafe { + let primary_monitor = MonitorFromPoint(POINT { x: 0, y: 0 }, MONITOR_DEFAULTTOPRIMARY); + let mut monitor_info: MONITORINFOEXW = std::mem::zeroed(); + monitor_info.cbSize = std::mem::size_of::() as u32; + + if GetMonitorInfoW(primary_monitor, &mut monitor_info as *mut _ as *mut _) != 0 { + let rect = monitor_info.rcMonitor; + let width = (rect.right - rect.left) as f32; + let height = (rect.bottom - rect.top) as f32; + + // Get DPI information + let dpi = GetDpiForSystem() as f32 / 96.0; // 96 is the default DPI + + // Convert device name to String + let device_name = String::from_utf16_lossy(&monitor_info.szDevice); + let clean_name = device_name.trim_end_matches('\0'); + + crate::MonitorMetrics { + width, + height, + position: (rect.left as u32, rect.top as u32), + dpi_scale: dpi, + refresh_rate: None, // Would need additional API calls to get refresh rate + name: Some(clean_name.to_string()), + } + } else { + // Fallback if monitor info fails + crate::MonitorMetrics { + width: GetSystemMetrics(SM_CXSCREEN) as f32, + height: GetSystemMetrics(SM_CYSCREEN) as f32, + position: (0, 0), + dpi_scale: 1.0, + refresh_rate: None, + name: Some("Primary Monitor".to_string()), + } + } + } +} + +pub fn monitors() -> Vec { + use winapi::shared::minwindef::*; + use winapi::shared::windef::*; + use winapi::um::winuser::*; + + unsafe { + static mut MONITORS: Vec = Vec::new(); + + unsafe extern "system" fn enum_monitor_proc( + monitor: HMONITOR, + _hdc: HDC, + _rect: LPRECT, + _lparam: LPARAM, + ) -> BOOL { + let mut monitor_info: MONITORINFOEXW = std::mem::zeroed(); + monitor_info.cbSize = std::mem::size_of::() as u32; + + if GetMonitorInfoW(monitor, &mut monitor_info as *mut _ as *mut _) != 0 { + let rect = monitor_info.rcMonitor; + let width = (rect.right - rect.left) as f32; + let height = (rect.bottom - rect.top) as f32; + + let dpi = GetDpiForSystem() as f32 / 96.0; + let device_name = String::from_utf16_lossy(&monitor_info.szDevice); + let clean_name = device_name.trim_end_matches('\0'); + + MONITORS.push(crate::MonitorMetrics { + width, + height, + position: (rect.left as u32, rect.top as u32), + dpi_scale: dpi, + refresh_rate: None, + name: Some(clean_name.to_string()), + }); + } + TRUE + } + + MONITORS.clear(); + EnumDisplayMonitors( + std::ptr::null_mut(), + std::ptr::null(), + Some(enum_monitor_proc), + 0, + ); + MONITORS.clone() + } +} + +pub fn current_monitor() -> crate::MonitorMetrics { + use winapi::shared::windef::*; + use winapi::um::winuser::*; + + unsafe { + // Get the current window handle - we need to find a way to get the HWND + // For now, let's use MonitorFromPoint with the current cursor position + let mut cursor_pos = POINT { x: 0, y: 0 }; + GetCursorPos(&mut cursor_pos); + + let monitor = MonitorFromPoint(cursor_pos, MONITOR_DEFAULTTONEAREST); + let mut monitor_info: MONITORINFOEXW = std::mem::zeroed(); + monitor_info.cbSize = std::mem::size_of::() as u32; + + if GetMonitorInfoW(monitor, &mut monitor_info as *mut _ as *mut _) != 0 { + let rect = monitor_info.rcMonitor; + let width = (rect.right - rect.left) as f32; + let height = (rect.bottom - rect.top) as f32; + + let dpi = GetDpiForSystem() as f32 / 96.0; + let device_name = String::from_utf16_lossy(&monitor_info.szDevice); + let clean_name = device_name.trim_end_matches('\0'); + + crate::MonitorMetrics { + width, + height, + position: (rect.left as u32, rect.top as u32), + dpi_scale: dpi, + refresh_rate: None, + name: Some(clean_name.to_string()), + } + } else { + // Fallback to primary monitor + primary_monitor() + } + } +} From e8e56a007e66830f18c2a8713f942872b191d56e Mon Sep 17 00:00:00 2001 From: Allen Date: Tue, 5 Aug 2025 21:41:54 +0800 Subject: [PATCH 07/10] Add window_position in Conf --- examples/window_position_test.rs | 29 +++++++++++++++++++++++++++++ src/conf.rs | 22 ++++++++++++++++++++++ src/native/linux_x11/libx11_ex.rs | 10 ++++++++-- src/native/macos.rs | 9 ++++++++- src/native/windows.rs | 13 +++++++++++-- 5 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 examples/window_position_test.rs diff --git a/examples/window_position_test.rs b/examples/window_position_test.rs new file mode 100644 index 000000000..492bd3b1b --- /dev/null +++ b/examples/window_position_test.rs @@ -0,0 +1,29 @@ +use miniquad::*; + +struct Stage { + ctx: GlContext, +} +impl EventHandler for Stage { + fn update(&mut self) {} + + fn draw(&mut self) { + self.ctx.clear(Some((0.2, 0.7, 0.3, 1.0)), None, None); + } +} + +fn main() { + miniquad::start( + conf::Conf { + window_title: "Window Position Test".to_string(), + window_width: 640, + window_height: 480, + window_position: Some((200, 150)), // Position window at (200, 150) + ..Default::default() + }, + || { + Box::new(Stage { + ctx: GlContext::new(), + }) + }, + ); +} diff --git a/src/conf.rs b/src/conf.rs index a7a3d4070..018ef24a7 100644 --- a/src/conf.rs +++ b/src/conf.rs @@ -40,6 +40,21 @@ //! //! [`dpi_scale`]: super::window::dpi_scale //! [`screen_size`]: super::window::screen_size +//! +//! ## Window positioning +//! +//! You can set the initial window position using the [`Conf::window_position`] field: +//! ```ignore +//! Conf { +//! window_width: 800, +//! window_height: 600, +//! window_position: Some((200, 100)), // Position at (200, 100) +//! ..Default::default() +//! } +//! ``` +//! +//! This feature only works on desktop platforms (Windows, macOS, Linux). +//! It is ignored on WASM, iOS, and Android platforms. /// Specifies how to load an OpenGL context on X11 in Linux. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] @@ -214,6 +229,11 @@ pub struct Conf { /// Defaults to `false`. pub fullscreen: bool, + /// Optional initial window position (x, y) in screen coordinates. + /// Only works on desktop platforms (Windows, macOS, Linux). + /// Ignored on WASM, iOS, and Android. Defaults to `None` (system default). + pub window_position: Option<(i32, i32)>, + /// MSAA sample count. /// Defaults to `1`. pub sample_count: i32, @@ -276,6 +296,7 @@ impl Default for Conf { window_resizable: true, icon: Some(Icon::miniquad_logo()), platform: Default::default(), + window_position: None, } } } @@ -293,6 +314,7 @@ impl Default for Conf { window_resizable: false, // icon: Some(Icon::miniquad_logo()), platform: Default::default(), + window_position: None, } } } diff --git a/src/native/linux_x11/libx11_ex.rs b/src/native/linux_x11/libx11_ex.rs index 7a859d861..3abf1d264 100644 --- a/src/native/linux_x11/libx11_ex.rs +++ b/src/native/linux_x11/libx11_ex.rs @@ -202,11 +202,17 @@ impl LibX11 { | PropertyChangeMask; self.grab_error_handler(); + let (win_x, win_y) = if let Some((x, y)) = conf.window_position { + (x as libc::c_int, y as libc::c_int) + } else { + (0 as libc::c_int, 0 as libc::c_int) + }; + let window = (self.XCreateWindow)( display, root, - 0 as libc::c_int, - 0 as libc::c_int, + win_x, + win_y, conf.window_width as _, conf.window_height as _, 0 as libc::c_int as libc::c_uint, diff --git a/src/native/macos.rs b/src/native/macos.rs index 88cfc74bb..8cdc7985a 100644 --- a/src/native/macos.rs +++ b/src/native/macos.rs @@ -1221,7 +1221,14 @@ where } let window_frame = NSRect { - origin: NSPoint { x: 0., y: 0. }, + origin: if let Some((x, y)) = conf.window_position { + NSPoint { + x: x as f64, + y: y as f64, + } + } else { + NSPoint { x: 0., y: 0. } + }, size: NSSize { width: conf.window_width as f64, height: conf.window_height as f64, diff --git a/src/native/windows.rs b/src/native/windows.rs index 5e180a3d1..2519b671a 100644 --- a/src/native/windows.rs +++ b/src/native/windows.rs @@ -641,6 +641,7 @@ unsafe fn create_window( resizable: bool, width: i32, height: i32, + position: Option<(i32, i32)>, ) -> (HWND, HDC) { let mut wndclassw: WNDCLASSW = std::mem::zeroed(); @@ -691,13 +692,20 @@ unsafe fn create_window( let class_name = "MINIQUADAPP\0".encode_utf16().collect::>(); let mut window_name = window_title.encode_utf16().collect::>(); window_name.push(0); + + let (win_x, win_y) = if let Some((x, y)) = position { + (x, y) + } else { + (CW_USEDEFAULT, CW_USEDEFAULT) + }; + let hwnd = CreateWindowExW( win_ex_style, // dwExStyle class_name.as_ptr(), // lpClassName window_name.as_ptr(), // lpWindowName win_style, // dwStyle - CW_USEDEFAULT, // X - CW_USEDEFAULT, // Y + win_x, // X + win_y, // Y win_width, // nWidth win_height, // nHeight NULL as _, // hWndParent @@ -895,6 +903,7 @@ where conf.window_resizable, conf.window_width as _, conf.window_height as _, + conf.window_position, ); if let Some(icon) = &conf.icon { set_icon(wnd, icon); From e329c6fd2bc7193a8cf9df38237115210cb2c5c7 Mon Sep 17 00:00:00 2001 From: Allen Date: Tue, 5 Aug 2025 22:00:48 +0800 Subject: [PATCH 08/10] Remove window_position from Conf, add desktop_center:bool --- examples/window_position_test.rs | 4 ++-- src/conf.rs | 14 +++++++------- src/native/linux_x11/libx11_ex.rs | 9 +++++++-- src/native/macos.rs | 31 ++++++++++++++++++++++--------- src/native/windows.rs | 13 +++++++++---- 5 files changed, 47 insertions(+), 24 deletions(-) diff --git a/examples/window_position_test.rs b/examples/window_position_test.rs index 492bd3b1b..a77019f6b 100644 --- a/examples/window_position_test.rs +++ b/examples/window_position_test.rs @@ -14,10 +14,10 @@ impl EventHandler for Stage { fn main() { miniquad::start( conf::Conf { - window_title: "Window Position Test".to_string(), + window_title: "Desktop Center Test".to_string(), window_width: 640, window_height: 480, - window_position: Some((200, 150)), // Position window at (200, 150) + desktop_center: true, // Center window on desktop ..Default::default() }, || { diff --git a/src/conf.rs b/src/conf.rs index 018ef24a7..f16ce1cfc 100644 --- a/src/conf.rs +++ b/src/conf.rs @@ -43,12 +43,12 @@ //! //! ## Window positioning //! -//! You can set the initial window position using the [`Conf::window_position`] field: +//! You can center the window on the desktop by setting [`Conf::desktop_center`]: //! ```ignore //! Conf { //! window_width: 800, //! window_height: 600, -//! window_position: Some((200, 100)), // Position at (200, 100) +//! desktop_center: true, // Center window on desktop //! ..Default::default() //! } //! ``` @@ -229,10 +229,10 @@ pub struct Conf { /// Defaults to `false`. pub fullscreen: bool, - /// Optional initial window position (x, y) in screen coordinates. + /// If `true`, center the window on the primary monitor. /// Only works on desktop platforms (Windows, macOS, Linux). - /// Ignored on WASM, iOS, and Android. Defaults to `None` (system default). - pub window_position: Option<(i32, i32)>, + /// Ignored on WASM, iOS, and Android. Defaults to `false`. + pub desktop_center: bool, /// MSAA sample count. /// Defaults to `1`. @@ -296,7 +296,7 @@ impl Default for Conf { window_resizable: true, icon: Some(Icon::miniquad_logo()), platform: Default::default(), - window_position: None, + desktop_center: false, } } } @@ -314,7 +314,7 @@ impl Default for Conf { window_resizable: false, // icon: Some(Icon::miniquad_logo()), platform: Default::default(), - window_position: None, + desktop_center: false, } } } diff --git a/src/native/linux_x11/libx11_ex.rs b/src/native/linux_x11/libx11_ex.rs index 3abf1d264..0ead5213f 100644 --- a/src/native/linux_x11/libx11_ex.rs +++ b/src/native/linux_x11/libx11_ex.rs @@ -202,8 +202,13 @@ impl LibX11 { | PropertyChangeMask; self.grab_error_handler(); - let (win_x, win_y) = if let Some((x, y)) = conf.window_position { - (x as libc::c_int, y as libc::c_int) + let (win_x, win_y) = if conf.desktop_center { + // For desktop centering, calculate center position + let screen_width = (self.XDisplayWidth)(display, screen) as i32; + let screen_height = (self.XDisplayHeight)(display, screen) as i32; + let center_x = (screen_width - conf.window_width) / 2; + let center_y = (screen_height - conf.window_height) / 2; + (center_x as libc::c_int, center_y as libc::c_int) } else { (0 as libc::c_int, 0 as libc::c_int) }; diff --git a/src/native/macos.rs b/src/native/macos.rs index 8cdc7985a..446d8fbc4 100644 --- a/src/native/macos.rs +++ b/src/native/macos.rs @@ -1221,14 +1221,7 @@ where } let window_frame = NSRect { - origin: if let Some((x, y)) = conf.window_position { - NSPoint { - x: x as f64, - y: y as f64, - } - } else { - NSPoint { x: 0., y: 0. } - }, + origin: NSPoint { x: 0., y: 0. }, size: NSSize { width: conf.window_width as f64, height: conf.window_height as f64, @@ -1287,7 +1280,6 @@ where assert!(!view.is_null()); - let () = msg_send![window, center]; let () = msg_send![window, setAcceptsMouseMovedEvents: YES]; // Register the view to accept dragged file URLs @@ -1304,6 +1296,27 @@ where msg_send_![window, orderFront: nil]; let () = msg_send![window, makeKeyAndOrderFront: nil]; + if conf.desktop_center { + // Get the primary monitor's frame for proper centering + unsafe { + let screen: ObjcId = msg_send![class!(NSScreen), mainScreen]; + let screen_frame: NSRect = msg_send![screen, frame]; + let window_frame: NSRect = msg_send![window, frame]; + + // Calculate center position + let center_x = + screen_frame.origin.x + (screen_frame.size.width - window_frame.size.width) / 2.0; + let center_y = + screen_frame.origin.y + (screen_frame.size.height - window_frame.size.height) / 2.0; + + // Set the window position + let mut new_frame = window_frame; + new_frame.origin.x = center_x; + new_frame.origin.y = center_y; + let () = msg_send![window, setFrame:new_frame display:true animate:false]; + } + } + let () = msg_send![ns_app, finishLaunching]; // Found this here: https://github.com/kovidgoyal/kitty/issues/6341#issuecomment-1578348104 diff --git a/src/native/windows.rs b/src/native/windows.rs index 2519b671a..69faf030e 100644 --- a/src/native/windows.rs +++ b/src/native/windows.rs @@ -641,7 +641,7 @@ unsafe fn create_window( resizable: bool, width: i32, height: i32, - position: Option<(i32, i32)>, + desktop_center: bool, ) -> (HWND, HDC) { let mut wndclassw: WNDCLASSW = std::mem::zeroed(); @@ -693,8 +693,13 @@ unsafe fn create_window( let mut window_name = window_title.encode_utf16().collect::>(); window_name.push(0); - let (win_x, win_y) = if let Some((x, y)) = position { - (x, y) + let (win_x, win_y) = if desktop_center { + // For desktop centering, calculate center position + let screen_width = GetSystemMetrics(SM_CXSCREEN); + let screen_height = GetSystemMetrics(SM_CYSCREEN); + let center_x = (screen_width - win_width) / 2; + let center_y = (screen_height - win_height) / 2; + (center_x, center_y) } else { (CW_USEDEFAULT, CW_USEDEFAULT) }; @@ -903,7 +908,7 @@ where conf.window_resizable, conf.window_width as _, conf.window_height as _, - conf.window_position, + conf.desktop_center, ); if let Some(icon) = &conf.icon { set_icon(wnd, icon); From 8950818454024fb9e6d377db608490e245a7a474 Mon Sep 17 00:00:00 2001 From: Allen Date: Tue, 5 Aug 2025 22:11:40 +0800 Subject: [PATCH 09/10] Fix CI build failure for linux --- src/native/linux_x11/libx11.rs | 3 +++ src/native/linux_x11/libx11_ex.rs | 1 + 2 files changed, 4 insertions(+) diff --git a/src/native/linux_x11/libx11.rs b/src/native/linux_x11/libx11.rs index d0c49621d..dcdbcd78a 100644 --- a/src/native/linux_x11/libx11.rs +++ b/src/native/linux_x11/libx11.rs @@ -928,6 +928,9 @@ crate::declare_module!( pub fn XCreatePixmapCursor(*mut Display, Pixmap, Pixmap, *mut XColor, *mut XColor, c_uint, c_uint) -> Cursor, pub fn XFreePixmap(*mut Display, Pixmap) -> c_int, pub fn XDefineCursor(*mut Display, Window, Cursor) -> c_int, + pub fn XDefaultScreen(*mut Display) -> c_int, + pub fn XDisplayWidth(*mut Display, c_int) -> c_int, + pub fn XDisplayHeight(*mut Display, c_int) -> c_int, ... ... pub extensions: X11Extensions, diff --git a/src/native/linux_x11/libx11_ex.rs b/src/native/linux_x11/libx11_ex.rs index 0ead5213f..75bb74d21 100644 --- a/src/native/linux_x11/libx11_ex.rs +++ b/src/native/linux_x11/libx11_ex.rs @@ -204,6 +204,7 @@ impl LibX11 { let (win_x, win_y) = if conf.desktop_center { // For desktop centering, calculate center position + let screen = (self.XDefaultScreen)(display); let screen_width = (self.XDisplayWidth)(display, screen) as i32; let screen_height = (self.XDisplayHeight)(display, screen) as i32; let center_x = (screen_width - conf.window_width) / 2; From 0830fc8d71b0c4e9b045242876d15629800181c4 Mon Sep 17 00:00:00 2001 From: Allen Date: Tue, 5 Aug 2025 22:19:24 +0800 Subject: [PATCH 10/10] Fix CI build for linux --- src/native/linux_x11.rs | 27 +++++++++++++++++++++------ src/native/linux_x11/libx11.rs | 1 + 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/native/linux_x11.rs b/src/native/linux_x11.rs index 9c4ce50b2..a80804139 100644 --- a/src/native/linux_x11.rs +++ b/src/native/linux_x11.rs @@ -704,7 +704,22 @@ where pub fn primary_monitor() -> crate::MonitorMetrics { unsafe { - let display = (LIBX11.XOpenDisplay)(std::ptr::null()); + // Create a temporary LibX11 instance for monitor queries + let mut libx11 = match libx11::LibX11::try_load() { + Ok(lib) => lib, + Err(_) => { + return crate::MonitorMetrics { + width: 1920.0, + height: 1080.0, + position: (0, 0), + dpi_scale: 1.0, + refresh_rate: None, + name: Some("Primary Monitor".to_string()), + }; + } + }; + + let display = (libx11.XOpenDisplay)(std::ptr::null()); if display.is_null() { return crate::MonitorMetrics { width: 1920.0, @@ -716,19 +731,19 @@ pub fn primary_monitor() -> crate::MonitorMetrics { }; } - let screen = (LIBX11.XDefaultScreen)(display); - let width = (LIBX11.XDisplayWidth)(display, screen) as f32; - let height = (LIBX11.XDisplayHeight)(display, screen) as f32; + let screen = (libx11.XDefaultScreen)(display); + let width = (libx11.XDisplayWidth)(display, screen) as f32; + let height = (libx11.XDisplayHeight)(display, screen) as f32; // Get DPI information - let width_mm = (LIBX11.XDisplayWidthMM)(display, screen) as f32; + let width_mm = (libx11.XDisplayWidthMM)(display, screen) as f32; let dpi_scale = if width_mm > 0.0 { (width * 25.4 / width_mm) / 96.0 // Convert to DPI scale factor } else { 1.0 }; - (LIBX11.XCloseDisplay)(display); + (libx11.XCloseDisplay)(display); crate::MonitorMetrics { width, diff --git a/src/native/linux_x11/libx11.rs b/src/native/linux_x11/libx11.rs index dcdbcd78a..672f06b97 100644 --- a/src/native/linux_x11/libx11.rs +++ b/src/native/linux_x11/libx11.rs @@ -931,6 +931,7 @@ crate::declare_module!( pub fn XDefaultScreen(*mut Display) -> c_int, pub fn XDisplayWidth(*mut Display, c_int) -> c_int, pub fn XDisplayHeight(*mut Display, c_int) -> c_int, + pub fn XDisplayWidthMM(*mut Display, c_int) -> c_int, ... ... pub extensions: X11Extensions,