1
0
mirror of https://github.com/emilk/egui.git synced 2026-06-26 22:53:14 -04:00

Merge branch 'main' into ime-preedit-visuals

This commit is contained in:
Umaĵo
2026-04-13 19:28:46 +08:00
committed by GitHub
17 changed files with 188 additions and 73 deletions

25
.github/workflows/taplo.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
# Checks that all TOML files are formatted with taplo.
name: Taplo
on:
push:
branches:
- "main"
pull_request:
types: [opened, synchronize]
jobs:
taplo:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Taplo
uses: taiki-e/install-action@v2.48.7
with:
tool: taplo-cli@0.9.3
- name: Check TOML formatting
run: |
taplo fmt --check

View File

@@ -1337,7 +1337,7 @@ dependencies = [
"image",
"jiff",
"mimalloc",
"rand 0.9.2",
"rand 0.9.3",
"serde",
"unicode_names2",
]
@@ -3657,9 +3657,9 @@ dependencies = [
[[package]]
name = "rand"
version = "0.9.2"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
checksum = "7ec095654a25171c2124e9e3393a930bddbffdc939556c914957a4c3e0a87166"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.3",

View File

@@ -134,12 +134,16 @@ syntect = { version = "5.3.0", default-features = false }
tempfile = "3.23.0"
thiserror = "2.0.17"
tokio = "1.49"
toml = {version = "1.0.0", default-features = false }
toml = { version = "1.0.0", default-features = false }
type-map = "0.5.1"
unicode_names2 = { version = "2.0.0", default-features = false }
unicode-general-category = "1.1.0"
unicode-segmentation = "1.12.0"
vello_cpu = { version = "0.0.7", default-features = false, features = ["std", "u8_pipeline", "f32_pipeline"] }
vello_cpu = { version = "0.0.7", default-features = false, features = [
"std",
"u8_pipeline",
"f32_pipeline",
] }
wasm-bindgen = "0.2.108" # Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml. Don't update this spuriously, because of https://github.com/rerun-io/rerun/issues/8766
wasm-bindgen-futures = "0.4.58"
wayland-cursor = { version = "0.31.11", default-features = false }

View File

@@ -722,6 +722,7 @@ impl WgpuWinitRunning<'_> {
&clipped_primitives,
&textures_delta,
screenshot_commands,
window,
);
for action in viewport.actions_requested.drain(..) {
@@ -1111,6 +1112,7 @@ fn render_immediate_viewport(
&clipped_primitives,
&textures_delta,
vec![],
window,
);
egui_winit.handle_platform_output(window, platform_output);

View File

@@ -56,8 +56,13 @@ impl TextAgent {
let input = input.clone();
move |event: web_sys::InputEvent, runner: &mut AppRunner| {
let text = input.value();
// Fix android virtual keyboard Gboard
// This removes the virtual keyboard's suggestion.
// Workaround for an Android Gboard issue: after typing a word,
// the user has to delete invisible characters (whose count
// matches the length of the current suggestion) before actual
// characters are deleted, unless the focus has been reset.
//
// this issue appears to have been fixed in Gboard sometime
// between versions 14.7.09 and 17.0.12.
if !event.is_composing() {
input.blur().ok();
input.focus().ok();
@@ -164,6 +169,12 @@ impl TextAgent {
let Some(ime) = ime else { return Ok(()) };
if ime.should_interrupt_composition {
// no-op for now: currently, the text agent is sizeless, so any
// click shifts focus to the canvas, which naturally interrupts the
// composition.
}
let mut canvas_rect = super::canvas_content_rect(canvas);
// Fix for safari with virtual keyboard flapping position
if is_mobile_safari() {

View File

@@ -411,6 +411,7 @@ impl Painter {
/// and the captures captured screenshot if it was requested.
///
/// If `capture_data` isn't empty, a screenshot will be captured.
#[expect(clippy::too_many_arguments)]
pub fn paint_and_update_textures(
&mut self,
viewport_id: ViewportId,
@@ -419,6 +420,7 @@ impl Painter {
clipped_primitives: &[epaint::ClippedPrimitive],
textures_delta: &epaint::textures::TexturesDelta,
capture_data: Vec<UserData>,
window: &winit::window::Window,
) -> f32 {
profiling::function_scope!();
@@ -654,6 +656,8 @@ impl Painter {
);
}
window.pre_present_notify();
{
profiling::scope!("present");
// wgpu doesn't document where vsync can happen. Maybe here?

View File

@@ -1057,7 +1057,8 @@ impl State {
self.set_cursor_icon(window, cursor_icon);
let allow_ime = ime.is_some();
if self.allow_ime != allow_ime {
let is_toggling_ime = self.allow_ime != allow_ime;
if is_toggling_ime {
self.allow_ime = allow_ime;
#[cfg(target_os = "windows")]
if !self.allow_ime {
@@ -1074,6 +1075,14 @@ impl State {
}
if let Some(ime) = ime {
if !is_toggling_ime && ime.should_interrupt_composition {
// TODO(umajho): use a more proper way to interrupt composition
// if `winit` provides one in the future.
window.set_ime_allowed(false);
window.set_ime_allowed(true);
}
let pixels_per_point = pixels_per_point(&self.egui_ctx, window);
let ime_rect_px = pixels_per_point * ime.rect;
if self.ime_rect_px != Some(ime_rect_px)

View File

@@ -793,7 +793,7 @@ impl Context {
let plugins = self.read(|ctx| ctx.plugins.ordered_plugins());
#[expect(deprecated)]
self.run(new_input, |ctx| {
let mut top_ui = Ui::new(
let mut root_ui = Ui::new(
ctx.clone(),
Id::new((ctx.viewport_id(), "__top_ui")),
UiBuilder::new()
@@ -802,14 +802,15 @@ impl Context {
);
{
plugins.on_begin_pass(&mut top_ui);
run_ui(&mut top_ui);
plugins.on_end_pass(&mut top_ui);
plugins.on_begin_pass(&mut root_ui);
run_ui(&mut root_ui);
plugins.on_end_pass(&mut root_ui);
}
// Inform ctx about what we actually used, so we can shrink the native window to fit.
// TODO(emilk): make better use of this somehow
ctx.pass_state_mut(|state| state.allocate_central_panel(top_ui.min_rect()));
ctx.pass_state_mut(|state| {
state.root_ui_available_rect = Some(root_ui.available_rect_before_wrap());
state.root_ui_min_rect = Some(root_ui.min_rect());
});
})
}
@@ -2611,6 +2612,12 @@ impl ContextImpl {
let mut platform_output: PlatformOutput = std::mem::take(&mut viewport.output);
if self.memory.should_interrupt_ime()
&& let Some(ime) = &mut platform_output.ime
{
ime.should_interrupt_composition = true;
}
{
profiling::scope!("accesskit");
let state = viewport.this_pass.accesskit_state.take();
@@ -2853,13 +2860,21 @@ impl Context {
/// How much space is still available after panels have been added.
#[deprecated = "Use content_rect (or viewport_rect) instead"]
pub fn available_rect(&self) -> Rect {
#[expect(deprecated)] // legacy
self.pass_state(|s| s.available_rect()).round_ui()
}
/// How much space is used by windows and the top-level [`Ui`].
pub fn globally_used_rect(&self) -> Rect {
self.write(|ctx| {
let mut used = ctx.viewport().this_pass.used_by_panels;
let viewport = ctx.viewport();
let root_ui_min_rect =
(viewport.this_pass.root_ui_min_rect).or(viewport.prev_pass.root_ui_min_rect);
let mut used = root_ui_min_rect.unwrap_or_else(|| {
#[expect(deprecated)] // legacy
ctx.viewport().this_pass.used_by_panels
});
for (_id, window) in ctx.memory.areas().visible_windows() {
used |= window.rect();
}
@@ -2886,18 +2901,27 @@ impl Context {
/// Is the pointer (mouse/touch) over any egui area?
pub fn is_pointer_over_egui(&self) -> bool {
let pointer_pos = self.input(|i| i.pointer.interact_pos());
if let Some(pointer_pos) = pointer_pos {
if let Some(layer) = self.layer_id_at(pointer_pos) {
if layer.order == Order::Background {
!self.pass_state(|state| state.unused_rect.contains(pointer_pos))
} else {
true
}
let Some(pointer_pos) = pointer_pos else {
return false;
};
let Some(layer) = self.layer_id_at(pointer_pos) else {
return false;
};
if layer.order == Order::Background {
let root_ui_available_rect = self
.pass_state(|state| state.root_ui_available_rect)
.or_else(|| self.prev_pass_state(|state| state.root_ui_available_rect));
if let Some(root_ui_available_rect) = root_ui_available_rect {
// Modern `run_ui` code
!root_ui_available_rect.contains(pointer_pos)
} else {
false
// Legacy code
#[expect(deprecated)]
!self.pass_state(|state| state.unused_rect.contains(pointer_pos))
}
} else {
false
true
}
}

View File

@@ -79,6 +79,9 @@ pub struct IMEOutput {
///
/// This is a very thin rectangle.
pub cursor_rect: crate::Rect,
/// Whether any ongoing IME composition should be interrupted.
pub should_interrupt_composition: bool,
}
/// Commands that the egui integration should execute at the end of a frame.

View File

@@ -117,21 +117,10 @@ pub struct Memory {
#[cfg_attr(feature = "persistence", serde(skip))]
popups: ViewportIdMap<OpenPopup>,
/// When the last IME interruption was made.
/// Whether to inform the backend to interrupt any ongoing IME composition
/// this pass.
#[cfg_attr(feature = "persistence", serde(skip))]
ime_interruption_time: ImeInterruptionTime,
}
#[derive(Clone, Copy, Debug, Default)]
enum ImeInterruptionTime {
#[default]
None,
/// The IME was interrupted in the current frame.
ThisFrame,
/// The IME was interrupted in the previous frame.
LastFrame,
requested_interrupt_ime: bool,
}
impl Default for Memory {
@@ -149,7 +138,7 @@ impl Default for Memory {
popups: Default::default(),
everything_is_visible: Default::default(),
add_fonts: Default::default(),
ime_interruption_time: Default::default(),
requested_interrupt_ime: Default::default(),
};
slf.interactions.entry(slf.viewport_id).or_default();
slf.areas.entry(slf.viewport_id).or_default();
@@ -778,15 +767,7 @@ impl Memory {
self.areas.entry(self.viewport_id).or_default();
match self.ime_interruption_time {
ImeInterruptionTime::ThisFrame => {
self.ime_interruption_time = ImeInterruptionTime::LastFrame;
}
ImeInterruptionTime::LastFrame => {
self.ime_interruption_time = ImeInterruptionTime::None;
}
ImeInterruptionTime::None => {}
}
self.requested_interrupt_ime = false;
// self.interactions is handled elsewhere
@@ -1028,30 +1009,22 @@ impl Memory {
///
/// A widget should only consume IME events if this returns `true`. At most
/// one widget can own IME events for each frame.
#[inline(always)]
pub fn owns_ime_events(&self, id: Id) -> bool {
let Some(focus) = self.focus() else {
return false;
};
// We check across two frames because the widget that called
// `interrupt_ime` may run after other widgets that call this method
// within the same frame.
if matches!(
self.ime_interruption_time,
ImeInterruptionTime::ThisFrame | ImeInterruptionTime::LastFrame
) {
return false;
}
focus.focused() == Some(id)
// Note: Even if the IME is being interrupted in the current frame, we
// should not return `false` here, since we still need
// `PlatformOutput::ime` to be set in such cases.
self.has_focus(id)
}
/// Interrupt the current IME composition, if any.
///
/// This causes [`Self::owns_ime_events`] to return `false` for all widgets
/// for the remainder of this frame and the next frame, giving time
/// for the IME to be dismissed (by making `platform_output.ime` be `None`
/// for at least one frame).
pub fn interrupt_ime(&mut self) {
self.ime_interruption_time = ImeInterruptionTime::ThisFrame;
self.requested_interrupt_ime = true;
}
pub(crate) fn should_interrupt_ime(&self) -> bool {
self.requested_interrupt_ime
}
}

View File

@@ -1,3 +1,5 @@
#![expect(deprecated)] // TODO(emilk): Remove legacy panels
use ahash::HashMap;
use crate::{Align, Id, IdMap, LayerId, Rangef, Rect, Vec2, WidgetRects, id::IdSet, style};
@@ -199,15 +201,28 @@ pub struct PassState {
pub tooltips: TooltipPassState,
/// What the root UI had available at the end of the previous pass.
///
/// Only set if [`crate::Context::run_ui`] has been called.
pub root_ui_available_rect: Option<Rect>,
/// What the root UI had used at the end of the previous pass.
///
/// Only set if [`crate::Context::run_ui`] has been called.
pub root_ui_min_rect: Option<Rect>,
/// Starts off as the `screen_rect`, shrinks as panels are added.
/// The [`crate::CentralPanel`] does not change this.
#[deprecated = "Only used by legacy Context-Panels"]
pub available_rect: Rect,
/// Starts off as the `screen_rect`, shrinks as panels are added.
/// The [`crate::CentralPanel`] retracts from this.
#[deprecated = "Only used by legacy Context-Panels"]
pub unused_rect: Rect,
/// How much space is used by panels.
#[deprecated = "Only used by legacy Context-Panels"]
pub used_by_panels: Rect,
/// The current scroll area should scroll to this range (horizontal, vertical).
@@ -240,6 +255,8 @@ impl Default for PassState {
widgets: Default::default(),
layers: Default::default(),
tooltips: Default::default(),
root_ui_available_rect: None,
root_ui_min_rect: None,
available_rect: Rect::NAN,
unused_rect: Rect::NAN,
used_by_panels: Rect::NAN,
@@ -262,6 +279,8 @@ impl PassState {
widgets,
tooltips,
layers,
root_ui_available_rect,
root_ui_min_rect,
available_rect,
unused_rect,
used_by_panels,
@@ -278,6 +297,8 @@ impl PassState {
widgets.clear();
tooltips.clear();
layers.clear();
*root_ui_available_rect = None;
*root_ui_min_rect = None;
*available_rect = content_rect;
*unused_rect = content_rect;
*used_by_panels = Rect::NOTHING;
@@ -295,6 +316,7 @@ impl PassState {
}
/// How much space is still available after panels has been added.
#[deprecated = "Only used by legacy Context-Panels"]
pub(crate) fn available_rect(&self) -> Rect {
debug_assert!(
self.available_rect.is_finite(),
@@ -304,6 +326,7 @@ impl PassState {
}
/// Shrink `available_rect`.
#[deprecated = "Only used by legacy Context-Panels"]
pub(crate) fn allocate_left_panel(&mut self, panel_rect: Rect) {
debug_assert!(
panel_rect.min.distance(self.available_rect.min) < 0.1,
@@ -315,6 +338,7 @@ impl PassState {
}
/// Shrink `available_rect`.
#[deprecated = "Only used by legacy Context-Panels"]
pub(crate) fn allocate_right_panel(&mut self, panel_rect: Rect) {
debug_assert!(
panel_rect.max.distance(self.available_rect.max) < 0.1,
@@ -326,6 +350,7 @@ impl PassState {
}
/// Shrink `available_rect`.
#[deprecated = "Only used by legacy Context-Panels"]
pub(crate) fn allocate_top_panel(&mut self, panel_rect: Rect) {
debug_assert!(
panel_rect.min.distance(self.available_rect.min) < 0.1,
@@ -337,6 +362,7 @@ impl PassState {
}
/// Shrink `available_rect`.
#[deprecated = "Only used by legacy Context-Panels"]
pub(crate) fn allocate_bottom_panel(&mut self, panel_rect: Rect) {
debug_assert!(
panel_rect.max.distance(self.available_rect.max) < 0.1,
@@ -347,6 +373,7 @@ impl PassState {
self.used_by_panels |= panel_rect;
}
#[deprecated = "Only used by legacy Context-Panels"]
pub(crate) fn allocate_central_panel(&mut self, panel_rect: Rect) {
// Note: we do not shrink `available_rect`, because
// we allow windows to cover the CentralPanel.

View File

@@ -894,6 +894,7 @@ impl TextEdit<'_> {
o.ime = Some(crate::output::IMEOutput {
rect: to_global * inner_rect,
cursor_rect: to_global * primary_cursor_rect,
should_interrupt_composition: false,
});
});
}

View File

@@ -11,7 +11,13 @@ readme = "README.md"
repository = "https://github.com/emilk/egui/tree/main/crates/egui_glow"
categories = ["gui", "game-development"]
keywords = ["glow", "egui", "gui", "gamedev"]
include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml", "src/shader/*.glsl"]
include = [
"../../LICENSE-APACHE",
"../../LICENSE-MIT",
"**/*.rs",
"Cargo.toml",
"src/shader/*.glsl",
]
[lints]
workspace = true
@@ -65,7 +71,6 @@ winit = { workspace = true, optional = true, default-features = false, features
web-sys = { workspace = true, features = ["console"] }
wasm-bindgen.workspace = true
[dev-dependencies]
glutin = { workspace = true, default-features = true } # examples/pure_glow
glutin-winit = { workspace = true, default-features = true }

View File

@@ -38,7 +38,7 @@ egui.workspace = true
eframe = { workspace = true, optional = true }
kittest.workspace = true
serde.workspace = true
toml = {workspace = true, features = ["parse", "serde"] }
toml = { workspace = true, features = ["parse", "serde"] }
# wgpu dependencies
egui-wgpu = { workspace = true, optional = true }

View File

@@ -48,7 +48,14 @@ mint = ["emath/mint"]
rayon = ["dep:rayon"]
## Allow serialization using [`serde`](https://docs.rs/serde).
serde = ["dep:serde", "ahash/serde", "emath/serde", "ecolor/serde", "font-types/serde", "smallvec/serde"]
serde = [
"dep:serde",
"ahash/serde",
"ecolor/serde",
"emath/serde",
"font-types/serde",
"smallvec/serde",
]
## Change Vertex layout to be compatible with unity
unity = []

View File

@@ -13,7 +13,8 @@ workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
eframe = { workspace = true, features = [
eframe = { workspace = true, default_features = false, features = [
"glow",
"default",
"__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO
] }

View File

@@ -30,6 +30,10 @@ struct MyTestApp {}
impl eframe::App for MyTestApp {
fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
egui::Panel::top("top").show_inside(ui, |ui| {
ui.label("This is a test of painting directly with glow.");
});
use glow::HasContext as _;
let gl = frame.gl().unwrap();
@@ -43,6 +47,21 @@ impl eframe::App for MyTestApp {
egui::Window::new("Floating Window").show(ui.ctx(), |ui| {
ui.label("The background should be purple.");
ui.label(format!(
"is_pointer_over_egui: {}",
ui.is_pointer_over_egui()
));
ui.label(format!(
"egui_wants_pointer_input: {}",
ui.egui_wants_pointer_input()
));
ui.label(format!(
"egui_is_using_pointer: {}",
ui.egui_is_using_pointer()
));
if let Some(pos) = ui.pointer_latest_pos() {
ui.label(format!("layer_id_at: {:?}", ui.layer_id_at(pos)));
}
});
}
}