1
0
mirror of https://github.com/emilk/egui.git synced 2026-09-02 06:40:06 -04:00

Merge branch 'main' into ime-preedit-visuals

This commit is contained in:
Umaĵo
2026-05-27 07:48:06 +08:00
committed by GitHub
230 changed files with 5317 additions and 4687 deletions

View File

@@ -32,7 +32,7 @@ use winit::{
dpi::{PhysicalPosition, PhysicalSize},
event::ElementState,
event_loop::ActiveEventLoop,
window::{CursorGrabMode, Window, WindowButtons, WindowLevel},
window::{CursorGrabMode, CustomCursor, Window, WindowButtons, WindowLevel},
};
pub fn screen_size_in_pixels(window: &Window) -> egui::Vec2 {
@@ -88,6 +88,14 @@ pub struct State {
any_pointer_button_down: bool,
current_cursor_icon: Option<egui::CursorIcon>,
/// Cached `CustomCursor` for the last RGBA bitmap pushed through
/// `PlatformOutput::cursor_image`. We dedupe by `Arc::as_ptr` so the
/// integration only re-uploads the bitmap to the OS when the app
/// switches sprite, not every frame the cursor moves. `usize` is the
/// raw pointer of the source `Arc<[u8]>` — opaque, only used as a
/// cache key.
current_custom_cursor: Option<(usize, CustomCursor)>,
clipboard: clipboard::Clipboard,
/// If `true`, mouse inputs will be treated as touches.
@@ -132,13 +140,16 @@ impl State {
};
let mut slf = Self {
egui_ctx,
viewport_id,
start_time: web_time::Instant::now(),
start_time: web_time::Instant::now()
.checked_sub(web_time::Duration::from_secs_f64(egui_ctx.time()))
.unwrap_or_else(web_time::Instant::now),
egui_ctx,
egui_input,
pointer_pos_in_points: None,
any_pointer_button_down: false,
current_cursor_icon: None,
current_custom_cursor: None,
clipboard: clipboard::Clipboard::new(
display_target.display_handle().ok().map(|h| h.as_raw()),
@@ -1036,12 +1047,38 @@ impl State {
&mut self,
window: &Window,
platform_output: egui::PlatformOutput,
) {
self.handle_platform_output_inner(window, None, platform_output);
}
/// Same as [`Self::handle_platform_output`] but threads the
/// `ActiveEventLoop` so we can register a `winit::CustomCursor` from
/// `PlatformOutput::cursor_image`. Integration paths that don't have
/// access to the event loop (e.g. immediate viewports) should call
/// [`Self::handle_platform_output`] instead — any custom cursor
/// request is silently dropped there and the standard `cursor_icon`
/// path still runs.
pub fn handle_platform_output_with_event_loop(
&mut self,
window: &Window,
event_loop: &ActiveEventLoop,
platform_output: egui::PlatformOutput,
) {
self.handle_platform_output_inner(window, Some(event_loop), platform_output);
}
fn handle_platform_output_inner(
&mut self,
window: &Window,
event_loop: Option<&ActiveEventLoop>,
platform_output: egui::PlatformOutput,
) {
profiling::function_scope!();
let egui::PlatformOutput {
commands,
cursor_icon,
cursor_image,
events: _, // handled elsewhere
mutable_text_under_cursor: _, // only used in eframe web
ime,
@@ -1064,7 +1101,7 @@ impl State {
}
}
self.set_cursor_icon(window, cursor_icon);
self.apply_cursor(window, event_loop, cursor_icon, cursor_image.as_ref());
let allow_ime = ime.is_some();
let is_toggling_ime = self.allow_ime != allow_ime;
@@ -1127,26 +1164,92 @@ impl State {
let _ = accesskit_update;
}
fn set_cursor_icon(&mut self, window: &Window, cursor_icon: egui::CursorIcon) {
/// Apply either a bitmap cursor (preferred when both `cursor_image`
/// and `event_loop` are `Some`) or the standard `cursor_icon` to the
/// window. Mirrors the no-flicker dedupe the old `set_cursor_icon`
/// did, on the appropriate cache key for whichever path is active.
fn apply_cursor(
&mut self,
window: &Window,
event_loop: Option<&ActiveEventLoop>,
cursor_icon: egui::CursorIcon,
cursor_image: Option<&egui::CustomCursorImage>,
) {
let is_pointer_in_window = self.pointer_pos_in_points.is_some();
if !is_pointer_in_window {
// Drop both caches so the cursor gets re-applied (and the
// bitmap re-checked for staleness) once the pointer comes
// back. Same contract the old `set_cursor_icon` followed.
self.current_cursor_icon = None;
self.current_custom_cursor = None;
return;
}
// Bitmap cursor wins over CursorIcon when both are present and we
// have an event loop to register it with. Otherwise the bitmap is
// dropped and we fall through to the icon path — this is the
// documented fallback for integrations that didn't opt in.
if let (Some(image), Some(event_loop)) = (cursor_image, event_loop) {
let key = std::sync::Arc::as_ptr(&image.rgba).cast::<u8>() as usize;
let cached = self
.current_custom_cursor
.as_ref()
.filter(|(k, _)| *k == key)
.map(|(_, c)| c.clone());
let custom = match cached {
Some(c) => c,
None => match winit::window::CustomCursor::from_rgba(
image.rgba.to_vec(),
image.size[0],
image.size[1],
image.hotspot[0],
image.hotspot[1],
) {
Ok(source) => {
let c = event_loop.create_custom_cursor(source);
self.current_custom_cursor = Some((key, c.clone()));
c
}
Err(err) => {
log::warn!(
"egui-winit: invalid cursor bitmap, falling back to cursor_icon: {err:?}"
);
self.current_custom_cursor = None;
self.set_cursor_icon_inner(window, cursor_icon);
return;
}
},
};
window.set_cursor_visible(true);
window.set_cursor(custom);
// Resync `current_cursor_icon` so the next icon-only path
// notices a real change rather than dedupe-skipping it.
self.current_cursor_icon = None;
return;
}
self.current_custom_cursor = None;
self.set_cursor_icon_inner(window, cursor_icon);
}
/// Icon-only path, factored out so `apply_cursor` can fall back to it
/// when the bitmap path bails. Preserves the original dedupe.
fn set_cursor_icon_inner(&mut self, window: &Window, cursor_icon: egui::CursorIcon) {
if self.current_cursor_icon == Some(cursor_icon) {
// Prevent flickering near frame boundary when Windows OS tries to control cursor icon for window resizing.
// On other platforms: just early-out to save CPU.
return;
}
let is_pointer_in_window = self.pointer_pos_in_points.is_some();
if is_pointer_in_window {
self.current_cursor_icon = Some(cursor_icon);
self.current_cursor_icon = Some(cursor_icon);
if let Some(winit_cursor_icon) = translate_cursor(cursor_icon) {
window.set_cursor_visible(true);
window.set_cursor(winit_cursor_icon);
} else {
window.set_cursor_visible(false);
}
if let Some(winit_cursor_icon) = translate_cursor(cursor_icon) {
window.set_cursor_visible(true);
window.set_cursor(winit_cursor_icon);
} else {
// Remember to set the cursor again once the cursor returns to the screen:
self.current_cursor_icon = None;
window.set_cursor_visible(false);
}
}
}
@@ -1696,7 +1799,24 @@ fn process_viewport_command(
ViewportCommand::Fullscreen(v) => {
window.set_fullscreen(v.then_some(winit::window::Fullscreen::Borderless(None)));
}
ViewportCommand::Decorations(v) => window.set_decorations(v),
ViewportCommand::SetMonitor(idx) => {
if let Some(monitor) = window.available_monitors().nth(idx) {
window.set_fullscreen(Some(winit::window::Fullscreen::Borderless(Some(monitor))));
} else {
log::warn!(
"ViewportCommand::SetMonitor({idx}): index out of range ({} monitors available)",
window.available_monitors().count()
);
}
}
ViewportCommand::Decorations(v) => {
window.set_decorations(v);
#[cfg(target_os = "windows")]
{
use winit::platform::windows::WindowExtWindows as _;
window.set_undecorated_shadow(!v);
}
}
ViewportCommand::WindowLevel(l) => window.set_window_level(match l {
egui::viewport::WindowLevel::AlwaysOnBottom => WindowLevel::AlwaysOnBottom,
egui::viewport::WindowLevel::AlwaysOnTop => WindowLevel::AlwaysOnTop,
@@ -1794,7 +1914,24 @@ pub fn create_window(
) -> Result<Window, winit::error::OsError> {
profiling::function_scope!();
let window_attributes = create_winit_window_attributes(egui_ctx, viewport_builder.clone());
let mut window_attributes = create_winit_window_attributes(egui_ctx, viewport_builder.clone());
// Resolve target monitor index → MonitorHandle, so the window is created
// directly in borderless fullscreen on the requested output. This is the
// only reliable way to target a specific monitor under Wayland, and also
// avoids the Mutter race where OuterPosition is ignored pre-mapping.
if let Some(idx) = viewport_builder.monitor {
if let Some(monitor) = event_loop.available_monitors().nth(idx) {
window_attributes = window_attributes
.with_fullscreen(Some(winit::window::Fullscreen::Borderless(Some(monitor))));
} else {
log::warn!(
"ViewportBuilder::with_monitor({idx}): index out of range ({} monitors available)",
event_loop.available_monitors().count()
);
}
}
let window = event_loop.create_window(window_attributes)?;
apply_viewport_builder_to_window(egui_ctx, &window, viewport_builder);
Ok(window)
@@ -1846,6 +1983,7 @@ pub fn create_winit_window_attributes(
mouse_passthrough: _, // handled in `apply_viewport_builder_to_window`
clamp_size_to_monitor_size: _, // Handled in `viewport_builder` in `epi_integration.rs`
monitor: _, // Handled in `create_window` (needs ActiveEventLoop for monitor handle)
} = viewport_builder;
let mut window_attributes = winit::window::WindowAttributes::default()
@@ -1976,6 +2114,7 @@ pub fn create_winit_window_attributes(
if let Some(show) = _taskbar {
window_attributes = window_attributes.with_skip_taskbar(!show);
}
window_attributes = window_attributes.with_undecorated_shadow(!decorations.unwrap_or(true));
}
#[cfg(target_os = "macos")]

View File

@@ -158,23 +158,30 @@ fn find_active_monitor(
return None; // no monitors 🤷
};
let mut active_monitor_overlap = 0.0;
for monitor in monitors {
let window_size_px = window_size_pts * (egui_zoom_factor * monitor.scale_factor() as f32);
let monitor_x_range = (monitor.position().x - window_size_px.x as i32)
..(monitor.position().x + monitor.size().width as i32);
let monitor_y_range = (monitor.position().y - window_size_px.y as i32)
..(monitor.position().y + monitor.size().height as i32);
let window_rect = egui::Rect::from_min_size(*position_px, window_size_px);
let overlap = window_rect.intersect(monitor_rect_px(&monitor)).area();
if monitor_x_range.contains(&(position_px.x as i32))
&& monitor_y_range.contains(&(position_px.y as i32))
{
if active_monitor_overlap < overlap {
active_monitor = monitor;
active_monitor_overlap = overlap;
}
}
Some(active_monitor)
}
fn monitor_rect_px(monitor: &winit::monitor::MonitorHandle) -> egui::Rect {
let pos = monitor.position();
let size = monitor.size();
egui::Rect::from_min_size(
egui::pos2(pos.x as f32, pos.y as f32),
egui::vec2(size.width as f32, size.height as f32),
)
}
fn clamp_pos_to_monitors(
egui_zoom_factor: f32,
event_loop: &winit::event_loop::ActiveEventLoop,
@@ -198,19 +205,12 @@ fn clamp_pos_to_monitors(
32.0 * egui_zoom_factor * active_monitor.scale_factor() as f32,
);
}
let monitor_position = egui::Pos2::new(
active_monitor.position().x as f32,
active_monitor.position().y as f32,
);
let monitor_size_px = egui::Vec2::new(
active_monitor.size().width as f32,
active_monitor.size().height as f32,
);
let monitor_rect = monitor_rect_px(&active_monitor);
// Window size cannot be negative or the subsequent `clamp` will panic.
let window_size = (monitor_size_px - window_size_px).max(egui::Vec2::ZERO);
let window_size = (monitor_rect.size() - window_size_px).max(egui::Vec2::ZERO);
// To get the maximum position, we get the rightmost corner of the display, then
// subtract the size of the window to get the bottom right most value window.position
// can have.
*position_px = position_px.clamp(monitor_position, monitor_position + window_size);
*position_px = position_px.clamp(monitor_rect.min, monitor_rect.min + window_size);
}