egui_desktop/utils/
rounded_corners.rs

1use eframe::Frame;
2use egui::Context;
3use raw_window_handle::{HasWindowHandle, RawWindowHandle};
4use std::{
5    collections::HashSet,
6    ffi::c_void,
7    sync::{LazyLock, Mutex, Once},
8};
9
10use crate::utils::os::apply_native_rounded_corners;
11
12// Track which viewports have had rounded corners applied
13static APPLIED_VIEWPORTS: LazyLock<Mutex<HashSet<egui::ViewportId>>> =
14    LazyLock::new(|| Mutex::new(HashSet::new()));
15
16/// Applies native rounded corners to the window if supported on the current platform.
17/// This should be called once after the window is created.
18///
19/// For the main window, use this function with the `Frame` from `eframe::App::update`.
20pub fn apply_rounded_corners(frame: &Frame) {
21    static INIT: Once = Once::new();
22
23    INIT.call_once(|| {
24        if let Ok(window_handle) = frame.window_handle() {
25            apply_rounded_corners_from_handle(window_handle);
26        }
27    });
28}
29
30/// Stores window handle information in the egui context for later use in viewport callbacks.
31/// This allows applying rounded corners to secondary viewports.
32///
33/// **Important**: This function stores the handle from the main window's Frame.
34/// For secondary viewports, you need to get the handle from the viewport's Frame.
35/// See `apply_rounded_corners_to_viewport` for a better approach.
36///
37/// # Arguments
38/// * `ctx` - The egui context
39/// * `frame` - The Frame containing the window handle to store
40/// * `viewport_id` - The ID of the viewport this handle is for
41pub fn store_window_handle_for_viewport(
42    ctx: &Context,
43    frame: &Frame,
44    viewport_id: egui::ViewportId,
45) {
46    if let Ok(window_handle) = frame.window_handle() {
47        let raw_handle: RawWindowHandle = window_handle.into();
48
49        // Extract the platform-specific pointer (which is Send + Sync)
50        let ptr: Option<*mut c_void> = match raw_handle {
51            RawWindowHandle::Win32(h) => Some(h.hwnd.get() as *mut _),
52            RawWindowHandle::AppKit(h) => Some(h.ns_view.as_ptr() as *mut _),
53            RawWindowHandle::Xlib(h) => Some(h.window as *mut _),
54            RawWindowHandle::Wayland(h) => Some(h.surface.as_ptr() as *mut _),
55            _ => None,
56        };
57
58        if let Some(native_ptr) = ptr {
59            let id = egui::Id::new(("rounded_corners_ptr", viewport_id));
60            // Store as usize (which is Send + Sync) instead of raw pointer
61            let ptr_as_usize = native_ptr as usize;
62            ctx.data_mut(|data| {
63                data.insert_temp(id, ptr_as_usize);
64            });
65        }
66    }
67}
68
69/// Applies native rounded corners to a viewport window.
70/// This should be called once per viewport after the window is created.
71///
72/// For secondary windows (viewports), use this function in the viewport's callback.
73/// It will only apply rounded corners once per viewport, but will reapply if the window
74/// was closed and reopened.
75///
76/// This function attempts multiple methods to get the window handle:
77/// 1. First tries to get a stored handle from the context
78/// 2. Then tries to get the handle using platform-specific APIs (by window title)
79///
80/// # Example
81///
82/// ```rust
83/// // In your viewport callback:
84/// ctx.show_viewport_deferred(viewport_id, viewport_builder, move |ctx, _class| {
85///     // Apply rounded corners (will attempt to find the window handle)
86///     apply_rounded_corners_to_viewport(ctx);
87///     // ... rest of your UI
88/// });
89/// ```
90pub fn apply_rounded_corners_to_viewport(ctx: &Context) {
91    let viewport_id = ctx.viewport_id();
92
93    // Check if viewport is being closed - if so, remove it from the applied list
94    // so it can be reapplied when reopened
95    if ctx.input(|i| i.viewport().close_requested()) {
96        let mut applied = APPLIED_VIEWPORTS.lock().unwrap();
97        applied.remove(&viewport_id);
98        // Also clear the stored handle
99        let id = egui::Id::new(("rounded_corners_ptr", viewport_id));
100        ctx.data_mut(|data| {
101            data.remove::<usize>(id);
102        });
103        return;
104    }
105
106    // Try to get a window handle - if we can get one, apply rounded corners
107    // We don't check if we've already applied because the window handle might have changed
108    // (e.g., if the window was closed and reopened, it's a new native window)
109
110    // Try method 1: Get stored window handle pointer from the context
111    let id = egui::Id::new(("rounded_corners_ptr", viewport_id));
112    let mut handle_found = false;
113
114    if let Some(ptr_as_usize) = ctx.data(|data| data.get_temp::<usize>(id)) {
115        // Convert back from usize to pointer
116        let native_ptr = ptr_as_usize as *mut c_void;
117
118        match apply_native_rounded_corners(native_ptr) {
119            Ok(_) => {
120                println!("🎉 Native rounded corners applied successfully to viewport!");
121                handle_found = true;
122            }
123            Err(e) => {
124                eprintln!(
125                    "âš ī¸ Failed to apply native rounded corners to viewport (stored handle): {}",
126                    e
127                );
128                // Handle might be invalid (window closed), try method 2
129            }
130        }
131    }
132
133    // Try method 2: Get window handle using platform-specific APIs
134    // This is especially useful when the window is reopened (new native window)
135    if !handle_found {
136        if let Some(native_ptr) = get_viewport_window_handle(ctx) {
137            match apply_native_rounded_corners(native_ptr) {
138                Ok(_) => {
139                    println!("🎉 Native rounded corners applied successfully to viewport!");
140                    // Store the new handle for future use
141                    let ptr_as_usize = native_ptr as usize;
142                    ctx.data_mut(|data| {
143                        data.insert_temp(id, ptr_as_usize);
144                    });
145                    handle_found = true;
146                }
147                Err(e) => {
148                    eprintln!(
149                        "âš ī¸ Failed to apply native rounded corners to viewport (found handle): {}",
150                        e
151                    );
152                }
153            }
154        }
155    }
156
157    if !handle_found {
158        eprintln!(
159            "âš ī¸ Could not apply rounded corners to viewport {:?}: Window handle not found. \
160             This may happen if the window hasn't been fully created yet.",
161            viewport_id
162        );
163    }
164}
165
166/// Attempts to get the window handle for a viewport using platform-specific APIs.
167/// This is a fallback when the handle isn't stored in the context.
168fn get_viewport_window_handle(ctx: &Context) -> Option<*mut c_void> {
169    // Get the viewport title to find the window
170    let viewport_title = ctx.input(|i| i.viewport().title.clone())?;
171
172    #[cfg(target_os = "windows")]
173    {
174        use std::ffi::OsStr;
175        use std::os::windows::ffi::OsStrExt;
176        use windows::Win32::UI::WindowsAndMessaging::{FindWindowW, GetWindowThreadProcessId};
177        use windows::core::PCWSTR;
178
179        // Convert title to wide string
180        let title_wide: Vec<u16> = OsStr::new(&viewport_title)
181            .encode_wide()
182            .chain(std::iter::once(0))
183            .collect();
184
185        unsafe {
186            // Try to find the window by its title
187            // FindWindowW returns Result<HWND, Error>, so we need to handle it
188            if let Ok(hwnd) = FindWindowW(None, PCWSTR::from_raw(title_wide.as_ptr())) {
189                if !hwnd.is_invalid() {
190                    // Verify it's actually our window by checking the process ID
191                    let mut process_id = 0u32;
192                    GetWindowThreadProcessId(hwnd, Some(&mut process_id));
193                    if process_id == std::process::id() {
194                        return Some(hwnd.0 as *mut c_void);
195                    }
196                }
197            }
198        }
199    }
200
201    #[cfg(target_os = "macos")]
202    {
203        use cocoa::appkit::NSWindow;
204        use cocoa::base::{id, nil};
205        use objc::{msg_send, sel, sel_impl};
206        use std::ffi::CString;
207
208        unsafe {
209            // Get all windows and find the one with matching title
210            let windows: id = msg_send![cocoa::appkit::NSApp(), windows];
211            let count: usize = msg_send![windows, count];
212
213            for i in 0..count {
214                let window: id = msg_send![windows, objectAtIndex: i];
215                let title: id = msg_send![window, title];
216
217                if title != nil {
218                    let title_str = cocoa::foundation::NSString::from_utf8_lossy(
219                        cocoa::foundation::NSString::UTF8String(title),
220                    );
221                    if title_str == viewport_title {
222                        // Get the content view
223                        let content_view: id = msg_send![window, contentView];
224                        if content_view != nil {
225                            return Some(content_view as *mut c_void);
226                        }
227                    }
228                }
229            }
230        }
231    }
232
233    // Linux/X11 and Wayland - not implemented yet
234    #[cfg(target_os = "linux")]
235    {
236        // TODO: Implement for Linux
237    }
238
239    None
240}
241
242/// Internal helper to apply rounded corners from a window handle
243fn apply_rounded_corners_from_handle(window_handle: raw_window_handle::WindowHandle) {
244    let handle: RawWindowHandle = window_handle.into();
245
246    let ptr: Option<*mut c_void> = match handle {
247        RawWindowHandle::Win32(h) => {
248            println!("đŸĒŸ Windows: Using Win32 window handle");
249            Some(h.hwnd.get() as *mut _)
250        }
251        RawWindowHandle::AppKit(h) => {
252            println!("🍎 macOS: Using AppKit window handle");
253            Some(h.ns_view.as_ptr() as *mut _)
254        }
255        RawWindowHandle::Xlib(h) => {
256            println!("🐧 Linux X11: Using Xlib window handle");
257            Some(h.window as *mut _)
258        }
259        RawWindowHandle::Wayland(h) => {
260            println!("🐧 Linux Wayland: Using Wayland surface handle");
261            Some(h.surface.as_ptr() as *mut _)
262        }
263        _ => {
264            println!(
265                "â„šī¸ Platform: Native rounded corners not supported for this window handle type: {:?}",
266                handle
267            );
268            None
269        }
270    };
271
272    if let Some(native_ptr) = ptr {
273        match apply_native_rounded_corners(native_ptr) {
274            Ok(_) => println!("🎉 Native rounded corners applied successfully!"),
275            Err(e) => eprintln!("âš ī¸ Failed to apply native rounded corners: {}", e),
276        }
277    }
278}