diff --git a/.github/workflows/taplo.yml b/.github/workflows/taplo.yml new file mode 100644 index 000000000..11a7b978a --- /dev/null +++ b/.github/workflows/taplo.yml @@ -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 diff --git a/Cargo.lock b/Cargo.lock index cd91fea8e..6efed68d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 0e592637c..4559152a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 } diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index 6d300d513..de691153f 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -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); diff --git a/crates/eframe/src/web/text_agent.rs b/crates/eframe/src/web/text_agent.rs index f4608f9b3..8f0bd695d 100644 --- a/crates/eframe/src/web/text_agent.rs +++ b/crates/eframe/src/web/text_agent.rs @@ -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() { diff --git a/crates/egui-wgpu/src/winit.rs b/crates/egui-wgpu/src/winit.rs index 3f6adfc27..d64644330 100644 --- a/crates/egui-wgpu/src/winit.rs +++ b/crates/egui-wgpu/src/winit.rs @@ -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, + 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? diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index 2a8778591..e10aeef07 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -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) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 824a5318c..433446648 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -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 } } diff --git a/crates/egui/src/data/output.rs b/crates/egui/src/data/output.rs index ea3ff4eec..77a0eee63 100644 --- a/crates/egui/src/data/output.rs +++ b/crates/egui/src/data/output.rs @@ -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. diff --git a/crates/egui/src/memory/mod.rs b/crates/egui/src/memory/mod.rs index 34e0fe319..8d109c254 100644 --- a/crates/egui/src/memory/mod.rs +++ b/crates/egui/src/memory/mod.rs @@ -117,21 +117,10 @@ pub struct Memory { #[cfg_attr(feature = "persistence", serde(skip))] popups: ViewportIdMap, - /// 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 } } diff --git a/crates/egui/src/pass_state.rs b/crates/egui/src/pass_state.rs index 56564541d..b6e655705 100644 --- a/crates/egui/src/pass_state.rs +++ b/crates/egui/src/pass_state.rs @@ -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, + + /// 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, + /// 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. diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 1f2d86a13..41519a99e 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -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, }); }); } diff --git a/crates/egui_glow/Cargo.toml b/crates/egui_glow/Cargo.toml index f714dd8e0..c5ff714b3 100644 --- a/crates/egui_glow/Cargo.toml +++ b/crates/egui_glow/Cargo.toml @@ -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 } diff --git a/crates/egui_kittest/Cargo.toml b/crates/egui_kittest/Cargo.toml index 10938cd37..2d26e6bd8 100644 --- a/crates/egui_kittest/Cargo.toml +++ b/crates/egui_kittest/Cargo.toml @@ -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 } diff --git a/crates/epaint/Cargo.toml b/crates/epaint/Cargo.toml index 705f1f6a9..f16903730 100644 --- a/crates/epaint/Cargo.toml +++ b/crates/epaint/Cargo.toml @@ -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 = [] diff --git a/tests/test_inline_glow_paint/Cargo.toml b/tests/test_inline_glow_paint/Cargo.toml index 090ccae4c..6aa9428e4 100644 --- a/tests/test_inline_glow_paint/Cargo.toml +++ b/tests/test_inline_glow_paint/Cargo.toml @@ -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 ] } diff --git a/tests/test_inline_glow_paint/src/main.rs b/tests/test_inline_glow_paint/src/main.rs index 576934bf3..fd4abc35f 100644 --- a/tests/test_inline_glow_paint/src/main.rs +++ b/tests/test_inline_glow_paint/src/main.rs @@ -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))); + } }); } }