diff --git a/.github/workflows/update_kittest_snapshots.yml b/.github/workflows/update_kittest_snapshots.yml index 9c67c52a9..bab1192d2 100644 --- a/.github/workflows/update_kittest_snapshots.yml +++ b/.github/workflows/update_kittest_snapshots.yml @@ -14,6 +14,7 @@ jobs: update-snapshots: name: Update snapshots from artifact runs-on: ubuntu-latest + if: github.ref_name != 'main' # We never want to update snapshots directly on main permissions: contents: write diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index 7cbaec624..54059cbd6 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -352,45 +352,7 @@ impl State { } WindowEvent::Ime(ime) => { - // on Mac even Cmd-C is pressed during ime, a `c` is pushed to Preedit. - // So no need to check is_mac_cmd. - // - // How winit produce `Ime::Enabled` and `Ime::Disabled` differs in MacOS - // and Windows. - // - // - On Windows, before and after each Commit will produce an Enable/Disabled - // event. - // - On MacOS, only when user explicit enable/disable ime. No Disabled - // after Commit. - // - // We use input_method_editor_started to manually insert CompositionStart - // between Commits. - match ime { - winit::event::Ime::Enabled => { - if cfg!(target_os = "linux") { - // This event means different things in X11 and Wayland, but we can just - // ignore it and enable IME on the preedit event. - // See - } else { - self.ime_event_enable(); - } - } - winit::event::Ime::Preedit(text, Some(_cursor)) => { - self.ime_event_enable(); - self.egui_input - .events - .push(egui::Event::Ime(egui::ImeEvent::Preedit(text.clone()))); - } - winit::event::Ime::Commit(text) => { - self.egui_input - .events - .push(egui::Event::Ime(egui::ImeEvent::Commit(text.clone()))); - self.ime_event_disable(); - } - winit::event::Ime::Disabled | winit::event::Ime::Preedit(_, None) => { - self.ime_event_disable(); - } - } + self.on_ime(ime); EventResponse { repaint: true, @@ -564,6 +526,104 @@ impl State { } } + /// ## NOTE + /// + /// on Mac even Cmd-C is pressed during ime, a `c` is pushed to Preedit. + /// So no need to check `is_mac_cmd`. + /// + /// ### How events are emitted by [`winit`] across different setups in various situations + /// + /// This is done by uncommenting the code block at the top of this method + /// and checking console outputs. + /// + /// winit version: 0.30.12. + /// + /// #### Setups + /// + /// - `a-macos15-apple_shuangpin`: macOS 15.7.3 `aarch64`, IME: builtin Chinese Shuangpin - Simplified. (Demo app shows: renderer: `wgpu`, backend: `Metal`.) + /// - `b-debian13_gnome48_wayland-fcitx5_shuangpin`: Debian 13 `aarch64`, Gnome 48, Wayland, IME: Fcitx5 with fcitx5-chinese-addons's Shuangpin. (Demo app shows: renderer: `wgpu`, backend: `Gl`.) + /// - `c-windows11-ms_pinyin`: Windows11 23H2 `x86_64`, IME: builtin Microsoft Pinyin. (Demo app shows: renderer: `wgpu`, backend: `Vulkan` & `Dx12`, others: `Dx12` & `Gl`.) + /// + /// #### Situation: pressed space to select the first candidate "测试" + /// + /// | Setup | Events in Order | + /// | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | + /// | a-macos15-apple_shuangpin | `Predict("", None)` -> `Commit("测试")` | + /// | b-debian13_gnome48_wayland-fcitx5_shuangpin | `Predict("", None)` -> `Commit("测试")` -> `Predict("", Some(0, 0))` -> `Predict("", None)` (duplicate until `TextEdit` blurred) | + /// | c-windows11-ms_pinyin | `Predict("测试", Some(…))` -> `Predict("", None)` -> `Commit("测试")` -> `Disabled` | + /// + /// #### Situation: pressed backspace to delete the last character in the prediction + /// + /// | Setup | Events in Order | + /// | a-macos15-apple_shuangpin | `Predict("", None)` | + /// | b-debian13_gnome48_wayland-fcitx5_shuangpin | `Predict("", Some(0, 0))` -> `Predict("", None)` (duplicate until `TextEdit` blurred) | + /// | c-windows11-ms_pinyin | `Predict("", Some(0, 0))` -> `Predict("", None)` -> `Commit("")` -> `Disabled` | + /// + /// #### Situation: clicked somewhere else while there is an active composition with the prediction "ce" + /// + /// | Setup | Events in Order | + /// | ------------------------------------------- | ------------------------------------------------------------------------------------------------- | + /// | a-macos15-apple_shuangpin | nothing emitted | + /// | b-debian13_gnome48_wayland-fcitx5_shuangpin | `Predict("", Some(0, 0))` (duplicate) -> `Predict("", None)` (duplicate until `TextEdit` blurred) | + /// | c-windows11-ms_pinyin | nothing emitted | + fn on_ime(&mut self, ime: &winit::event::Ime) { + // // code for inspecting ime events emitted by winit: + // { + // static LAST_IME: std::sync::Mutex> = + // std::sync::Mutex::new(None); + // static IS_LAST_DUPLICATE: std::sync::atomic::AtomicBool = + // std::sync::atomic::AtomicBool::new(false); + // let mut last_ime_guard = LAST_IME.lock().unwrap(); + // if { last_ime_guard.as_ref().cloned() }.as_ref() != Some(ime) { + // println!("IME={ime:?}"); + // *last_ime_guard = Some(ime.clone()); + // IS_LAST_DUPLICATE.store(false, std::sync::atomic::Ordering::Relaxed); + // } else if !IS_LAST_DUPLICATE.load(std::sync::atomic::Ordering::Relaxed) { + // println!("IME=(duplicate)"); + // IS_LAST_DUPLICATE.store(true, std::sync::atomic::Ordering::Relaxed); + // } + // } + + match ime { + winit::event::Ime::Enabled => { + if cfg!(target_os = "linux") { + // This event means different things in X11 and Wayland, but we can just + // ignore it and enable IME on the preedit event. + // See + } else { + self.ime_event_enable(); + } + } + winit::event::Ime::Preedit(text, Some(_cursor)) => { + self.ime_event_enable(); + self.egui_input + .events + .push(egui::Event::Ime(egui::ImeEvent::Preedit(text.clone()))); + } + winit::event::Ime::Commit(text) => { + self.egui_input + .events + .push(egui::Event::Ime(egui::ImeEvent::Commit(text.clone()))); + self.ime_event_disable(); + } + winit::event::Ime::Disabled => { + self.ime_event_disable(); + } + winit::event::Ime::Preedit(_, None) => { + // we need to emit this on macOS, since winit doesn't emit + // `Predict("", Some(0, 0))` before this event on macOS when the + // user deletes the last character in the prediction with the + // backspace key. Without this, only `egui::ImeEvent::Disabled` + // is emitted here, leading to the last character being left in + // TextEdit in such situation. + self.egui_input + .events + .push(egui::Event::Ime(egui::ImeEvent::Preedit(String::new()))); + self.ime_event_disable(); + } + } + } + pub fn ime_event_enable(&mut self) { if !self.has_sent_ime_enabled { self.egui_input @@ -1668,6 +1728,7 @@ pub fn create_winit_window_attributes( // x11 window_type: _window_type, + override_redirect: _override_redirect, mouse_passthrough: _, // handled in `apply_viewport_builder_to_window` clamp_size_to_monitor_size: _, // Handled in `viewport_builder` in `epi_integration.rs` @@ -1767,8 +1828,8 @@ pub fn create_winit_window_attributes( #[cfg(all(feature = "x11", target_os = "linux"))] { + use winit::platform::x11::WindowAttributesExtX11 as _; if let Some(window_type) = _window_type { - use winit::platform::x11::WindowAttributesExtX11 as _; use winit::platform::x11::WindowType; window_attributes = window_attributes.with_x11_window_type(vec![match window_type { egui::X11WindowType::Normal => WindowType::Normal, @@ -1787,6 +1848,9 @@ pub fn create_winit_window_attributes( egui::X11WindowType::Dnd => WindowType::Dnd, }]); } + if let Some(override_redirect) = _override_redirect { + window_attributes = window_attributes.with_override_redirect(override_redirect); + } } #[cfg(target_os = "windows")] diff --git a/crates/egui/src/cache/cache_storage.rs b/crates/egui/src/cache/cache_storage.rs index 255eca2d5..e0aa65e8c 100644 --- a/crates/egui/src/cache/cache_storage.rs +++ b/crates/egui/src/cache/cache_storage.rs @@ -19,7 +19,7 @@ use super::CacheTrait; /// /// # let mut cache_storage = CacheStorage::default(); /// let mut cache = cache_storage.cache::>(); -/// assert_eq!(cache.get("hello"), 5); +/// assert_eq!(*cache.get("hello"), 5); /// ``` #[derive(Default)] pub struct CacheStorage { @@ -28,11 +28,13 @@ pub struct CacheStorage { impl CacheStorage { pub fn cache(&mut self) -> &mut Cache { - #[expect(clippy::unwrap_used)] - self.caches + let cache = self + .caches .entry(std::any::TypeId::of::()) - .or_insert_with(|| Box::::default()) - .as_any_mut() + .or_insert_with(|| Box::::default()); + + #[expect(clippy::unwrap_used)] + (cache.as_mut() as &mut dyn std::any::Any) .downcast_mut::() .unwrap() } diff --git a/crates/egui/src/cache/cache_trait.rs b/crates/egui/src/cache/cache_trait.rs index fc00a9880..54144c724 100644 --- a/crates/egui/src/cache/cache_trait.rs +++ b/crates/egui/src/cache/cache_trait.rs @@ -1,11 +1,9 @@ /// A cache, storing some value for some length of time. #[expect(clippy::len_without_is_empty)] -pub trait CacheTrait: 'static + Send + Sync { +pub trait CacheTrait: 'static + Send + Sync + std::any::Any { /// Call once per frame to evict cache. fn update(&mut self); /// Number of values currently in the cache. fn len(&self) -> usize; - - fn as_any_mut(&mut self) -> &mut dyn std::any::Any; } diff --git a/crates/egui/src/cache/frame_cache.rs b/crates/egui/src/cache/frame_cache.rs index 6c74c58dc..ae39712c7 100644 --- a/crates/egui/src/cache/frame_cache.rs +++ b/crates/egui/src/cache/frame_cache.rs @@ -46,10 +46,9 @@ impl FrameCache { impl FrameCache { /// Get from cache (if the same key was used last frame) /// or recompute and store in the cache. - pub fn get(&mut self, key: Key) -> Value + pub fn get(&mut self, key: Key) -> &Value where Key: Copy + std::hash::Hash, - Value: Clone, Computer: ComputerMut, { let hash = crate::util::hash(key); @@ -58,12 +57,12 @@ impl FrameCache { std::collections::hash_map::Entry::Occupied(entry) => { let cached = entry.into_mut(); cached.0 = self.generation; - cached.1.clone() + &cached.1 } std::collections::hash_map::Entry::Vacant(entry) => { let value = self.computer.compute(key); - entry.insert((self.generation, value.clone())); - value + let inserted = entry.insert((self.generation, value)); + &inserted.1 } } } @@ -79,8 +78,4 @@ impl CacheTrait fn len(&self) -> usize { self.cache.len() } - - fn as_any_mut(&mut self) -> &mut dyn std::any::Any { - self - } } diff --git a/crates/egui/src/cache/frame_publisher.rs b/crates/egui/src/cache/frame_publisher.rs index 0c2bc81d6..81ba34df6 100644 --- a/crates/egui/src/cache/frame_publisher.rs +++ b/crates/egui/src/cache/frame_publisher.rs @@ -54,8 +54,4 @@ where fn len(&self) -> usize { self.cache.len() } - - fn as_any_mut(&mut self) -> &mut dyn std::any::Any { - self - } } diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index 3b6d4006e..4333cf73a 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -505,9 +505,9 @@ impl Area { let interact_id = layer_id.id.with("move"); let sense = sense.unwrap_or_else(|| { if movable { - Sense::drag() + Sense::DRAG } else if interactable { - Sense::click() // allow clicks to bring to front + Sense::CLICK // allow clicks to bring to front } else { Sense::hover() } diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index afe1239c7..8f46223d2 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -810,7 +810,7 @@ impl ScrollArea { // or we will steal input from the widgets we contain. let content_response_option = state .interact_rect - .map(|rect| ui.interact(rect, id.with("area"), Sense::drag())); + .map(|rect| ui.interact(rect, id.with("area"), Sense::DRAG)); if content_response_option .as_ref() @@ -1276,7 +1276,7 @@ impl Prepared { }; let sense = if scroll_source.scroll_bar && ui.is_enabled() { - Sense::click_and_drag() + Sense::CLICK | Sense::DRAG } else { Sense::hover() }; diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index 6dc7927ae..c6b739589 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -937,7 +937,7 @@ fn move_and_resize_window(ctx: &Context, id: Id, interaction: &ResizeInteraction fn do_resize_interaction( ctx: &Context, possible: PossibleInteractions, - _accessibility_parent: Id, + accessibility_parent: Id, layer_id: LayerId, outer_rect: Rect, window_frame: Frame, @@ -957,14 +957,14 @@ fn do_resize_interaction( let rect = outer_rect.shrink(window_frame.stroke.width / 2.0); let side_response = |rect, id| { - ctx.register_accesskit_parent(id, _accessibility_parent); + ctx.register_accesskit_parent(id, accessibility_parent); let response = ctx.create_widget( WidgetRect { layer_id, id, rect, interact_rect: rect, - sense: Sense::drag(), + sense: Sense::DRAG, // Don't use Sense::drag() since we don't want these to be focusable enabled: true, }, true, @@ -1324,7 +1324,7 @@ impl TitleBar { let id = ui.unique_id().with("__window_title_bar"); if ui - .interact(double_click_rect, id, Sense::click()) + .interact(double_click_rect, id, Sense::CLICK) .double_clicked() && collapsible { diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index f8996b8a3..e28048edf 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -2432,7 +2432,8 @@ impl Context { if let Some(widget) = self.write(|ctx| ctx.viewport().this_pass.widgets.get(id).copied()) { - paint_widget(&widget, text, color); + let text = format!("{text} - {id:?}"); + paint_widget(&widget, &text, color); } }; @@ -2541,6 +2542,12 @@ impl Context { } } + if self.global_style().debug.show_focused_widget + && let Some(focused_id) = self.memory(|mem| mem.focused()) + { + paint_widget_id(focused_id, "focused", Color32::PURPLE); + } + if let Some(debug_rect) = self.pass_state_mut(|fs| fs.debug_rect.take()) { debug_rect.paint(&self.debug_painter()); } diff --git a/crates/egui/src/data/input.rs b/crates/egui/src/data/input.rs index 61f6ae00c..787867569 100644 --- a/crates/egui/src/data/input.rs +++ b/crates/egui/src/data/input.rs @@ -608,6 +608,13 @@ pub const NUM_POINTER_BUTTONS: usize = 5; /// /// The best way to compare [`Modifiers`] is by using [`Modifiers::matches_logically`] or [`Modifiers::matches_exact`]. /// +/// To access the [`Modifiers`] you can use the [`crate::Context::input`] function +/// +/// ```rust +/// # let ctx = egui::Context::default(); +/// let modifiers = ctx.input(|i| i.modifiers); +/// ``` +/// /// NOTE: For cross-platform uses, ALT+SHIFT is a bad combination of modifiers /// as on mac that is how you type special characters, /// so those key presses are usually not reported to egui. diff --git a/crates/egui/src/input_state/mod.rs b/crates/egui/src/input_state/mod.rs index bec4bec59..49027c06e 100644 --- a/crates/egui/src/input_state/mod.rs +++ b/crates/egui/src/input_state/mod.rs @@ -970,6 +970,15 @@ impl PointerEvent { } /// Mouse or touch state. +/// +/// To access the methods of [`PointerState`] you can use the [`crate::Context::input`] function +/// +/// ```rust +/// # let ctx = egui::Context::default(); +/// let latest_pos = ctx.input(|i| i.pointer.latest_pos()); +/// let is_pointer_down = ctx.input(|i| i.pointer.any_down()); +/// ``` +/// #[derive(Clone, Debug)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct PointerState { @@ -1028,6 +1037,10 @@ pub struct PointerState { /// This could also be the trigger point for a long-touch. pub(crate) started_decidedly_dragging: bool, + /// Where did the last click originate? + /// `None` if no mouse click occurred. + last_click_pos: Option, + /// When did the pointer get click last? /// Used to check for double-clicks. last_click_time: f64, @@ -1065,6 +1078,7 @@ impl Default for PointerState { press_start_time: None, has_moved_too_much_for_a_click: false, started_decidedly_dragging: false, + last_click_pos: None, last_click_time: f64::NEG_INFINITY, last_last_click_time: f64::NEG_INFINITY, last_move_time: f64::NEG_INFINITY, @@ -1140,10 +1154,18 @@ impl PointerState { let clicked = self.could_any_button_be_click(); let click = if clicked { - let double_click = - (time - self.last_click_time) < self.options.max_double_click_delay; + let click_dist_sq = self + .last_click_pos + .map_or(0.0, |last_pos| last_pos.distance_sq(pos)); + + let double_click = (time - self.last_click_time) + < self.options.max_double_click_delay + && click_dist_sq + < self.options.max_click_dist * self.options.max_click_dist; let triple_click = (time - self.last_last_click_time) - < (self.options.max_double_click_delay * 2.0); + < (self.options.max_double_click_delay * 2.0) + && click_dist_sq + < self.options.max_click_dist * self.options.max_click_dist; let count = if triple_click { 3 } else if double_click { @@ -1154,6 +1176,7 @@ impl PointerState { self.last_last_click_time = self.last_click_time; self.last_click_time = time; + self.last_click_pos = Some(pos); Some(Click { pos, @@ -1621,6 +1644,7 @@ impl PointerState { press_start_time, has_moved_too_much_for_a_click, started_decidedly_dragging, + last_click_pos, last_click_time, last_last_click_time, pointer_events, @@ -1646,6 +1670,7 @@ impl PointerState { ui.label(format!( "started_decidedly_dragging: {started_decidedly_dragging}" )); + ui.label(format!("last_click_pos: {last_click_pos:#?}")); ui.label(format!("last_click_time: {last_click_time:#?}")); ui.label(format!("last_last_click_time: {last_last_click_time:#?}")); ui.label(format!("last_move_time: {last_move_time:#?}")); diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 4f310ba8f..bc90f0cf9 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -413,7 +413,7 @@ pub mod os; mod painter; mod pass_state; pub(crate) mod placer; -mod plugin; +pub mod plugin; pub mod response; mod sense; pub mod style; diff --git a/crates/egui/src/load.rs b/crates/egui/src/load.rs index fd15322fb..99909eca1 100644 --- a/crates/egui/src/load.rs +++ b/crates/egui/src/load.rs @@ -3,7 +3,7 @@ //! If you just want to display some images, [`egui_extras`](https://crates.io/crates/egui_extras/) //! will get you up and running quickly with its reasonable default implementations of the traits described below. //! -//! 1. Add [`egui_extras`](https://crates.io/crates/egui_extras/) as a dependency with the `all_loaders` feature. +//! 1. Add [`egui_extras`](https://crates.io/crates/egui_extras/) as a dependency with the `all_loaders` feature (`cargo add egui_extras -F all_loaders`). //! 2. Add a call to [`egui_extras::install_image_loaders`](https://docs.rs/egui_extras/latest/egui_extras/fn.install_image_loaders.html) //! in your app's setup code. //! 3. Use [`Ui::image`][`crate::ui::Ui::image`] with some [`ImageSource`][`crate::ImageSource`]. diff --git a/crates/egui/src/memory/mod.rs b/crates/egui/src/memory/mod.rs index defb14a39..9c91c1322 100644 --- a/crates/egui/src/memory/mod.rs +++ b/crates/egui/src/memory/mod.rs @@ -68,7 +68,7 @@ pub struct Memory { /// # let mut ctx = egui::Context::default(); /// ctx.memory_mut(|mem| { /// let cache = mem.caches.cache::>(); - /// assert_eq!(cache.get("hello"), 5); + /// assert_eq!(*cache.get("hello"), 5); /// }); /// ``` #[cfg_attr(feature = "persistence", serde(skip))] diff --git a/crates/egui/src/plugin.rs b/crates/egui/src/plugin.rs index 3967e737d..6bef04123 100644 --- a/crates/egui/src/plugin.rs +++ b/crates/egui/src/plugin.rs @@ -55,6 +55,9 @@ pub(crate) struct PluginHandle { plugin: Box, } +/// A typed handle to a registered [`Plugin`]. +/// +/// Use [`Self::lock`] to access the plugin. pub struct TypedPluginHandle { handle: Arc>, _type: std::marker::PhantomData

, @@ -68,6 +71,9 @@ impl TypedPluginHandle

{ } } + /// Lock the plugin for access. + /// + /// Returns a guard that dereferences to the plugin. pub fn lock(&self) -> TypedPluginGuard<'_, P> { TypedPluginGuard { guard: self.handle.lock(), @@ -76,6 +82,7 @@ impl TypedPluginHandle

{ } } +/// A guard that provides access to a [`Plugin`]. pub struct TypedPluginGuard<'a, P: Plugin> { guard: MutexGuard<'a, PluginHandle>, _type: std::marker::PhantomData

, @@ -113,13 +120,13 @@ impl PluginHandle { } fn typed_plugin(&self) -> &P { - (&*self.plugin as &dyn std::any::Any) + (self.plugin.as_ref() as &dyn std::any::Any) .downcast_ref::

() .expect("PluginHandle: plugin is not of the expected type") } pub fn typed_plugin_mut(&mut self) -> &mut P { - (&mut *self.plugin as &mut dyn std::any::Any) + (self.plugin.as_mut() as &mut dyn std::any::Any) .downcast_mut::

() .expect("PluginHandle: plugin is not of the expected type") } diff --git a/crates/egui/src/sense.rs b/crates/egui/src/sense.rs index 464db311f..c3b3af7f2 100644 --- a/crates/egui/src/sense.rs +++ b/crates/egui/src/sense.rs @@ -53,19 +53,23 @@ impl Sense { Self::FOCUSABLE } - /// Sense clicks and hover, but not drags. + /// Sense clicks and hover, but not drags, and make the widget focusable. + /// + /// Use [`Sense::CLICK`] if you don't want the widget to be focusable. #[inline] pub fn click() -> Self { Self::CLICK | Self::FOCUSABLE } - /// Sense drags and hover, but not clicks. + /// Sense drags and hover, but not clicks. Make the widget focusable. + /// + /// Use [`Sense::DRAG`] if you don't want the widget to be focusable #[inline] pub fn drag() -> Self { Self::DRAG | Self::FOCUSABLE } - /// Sense both clicks, drags and hover (e.g. a slider or window). + /// Sense both clicks, drags and hover (e.g. a slider or window), and make the widget focusable. /// /// Note that this will introduce a latency when dragging, /// because when the user starts a press egui can't know if this is the start diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index f94b9d345..a555b9ace 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -1303,6 +1303,13 @@ pub struct DebugOptions { /// /// See [`emath::GuiRounding`] for more. pub show_unaligned: bool, + + /// Highlight the currently focused widget. + /// + /// This is useful when some widget has a invisible focus (e.g. when a widget is using + /// `Sense::click()` when it should be using `Sense::CLICK`) and you need to find which one it + /// is. + pub show_focused_widget: bool, } #[cfg(debug_assertions)] @@ -1319,6 +1326,7 @@ impl Default for DebugOptions { show_interactive_widgets: false, show_widget_hits: false, show_unaligned: cfg!(debug_assertions), + show_focused_widget: false, } } } @@ -2480,6 +2488,7 @@ impl DebugOptions { show_interactive_widgets, show_widget_hits, show_unaligned, + show_focused_widget, } = self; { @@ -2514,6 +2523,11 @@ impl DebugOptions { "Show rectangles not aligned to integer point coordinates", ); + ui.checkbox( + show_focused_widget, + "Highlight which widget has keyboard focus", + ); + ui.vertical_centered(|ui| reset_button(ui, self, "Reset debug options")); } } diff --git a/crates/egui/src/viewport.rs b/crates/egui/src/viewport.rs index 15741c017..ef6ee6c63 100644 --- a/crates/egui/src/viewport.rs +++ b/crates/egui/src/viewport.rs @@ -332,6 +332,7 @@ pub struct ViewportBuilder { // X11 pub window_type: Option, + pub override_redirect: Option, } impl ViewportBuilder { @@ -663,13 +664,22 @@ impl ViewportBuilder { /// ### On X11 /// This sets the window type. - /// Maps directly to [`_NET_WM_WINDOW_TYPE`](https://specifications.freedesktop.org/wm-spec/wm-spec-1.5.html). + /// Maps directly to [`_NET_WM_WINDOW_TYPE`](https://specifications.freedesktop.org/wm/1.5/ar01s05.html#id-1.6.7). #[inline] pub fn with_window_type(mut self, value: X11WindowType) -> Self { self.window_type = Some(value); self } + /// ### On X11 + /// This sets the override-redirect flag. When this is set to true the window type should be specified. + /// Maps directly to [`Override-redirect windows`](https://specifications.freedesktop.org/wm/1.5/ar01s02.html#id-1.3.13). + #[inline] + pub fn with_override_redirect(mut self, value: bool) -> Self { + self.override_redirect = Some(value); + self + } + /// Update this `ViewportBuilder` with a delta, /// returning a list of commands and a bool indicating if the window needs to be recreated. #[must_use] @@ -706,6 +716,7 @@ impl ViewportBuilder { mouse_passthrough: new_mouse_passthrough, taskbar: new_taskbar, window_type: new_window_type, + override_redirect: new_override_redirect, } = new_vp_builder; let mut commands = Vec::new(); @@ -903,6 +914,11 @@ impl ViewportBuilder { recreate_window = true; } + if new_override_redirect.is_some() && self.override_redirect != new_override_redirect { + self.override_redirect = new_override_redirect; + recreate_window = true; + } + (commands, recreate_window) } } diff --git a/crates/egui/src/widget_rect.rs b/crates/egui/src/widget_rect.rs index e03423623..a84dde519 100644 --- a/crates/egui/src/widget_rect.rs +++ b/crates/egui/src/widget_rect.rs @@ -164,6 +164,8 @@ impl WidgetRects { let InteractOptions { move_to_top } = options; + let mut shift_layer_index_after = None; + let layer_widgets = by_layer.entry(layer_id).or_default(); match by_id.entry(widget_rect.id) { @@ -187,6 +189,7 @@ impl WidgetRects { if existing.layer_id == widget_rect.layer_id { if move_to_top { layer_widgets.remove(*idx_in_layer); + shift_layer_index_after = Some(*idx_in_layer); *idx_in_layer = layer_widgets.len(); layer_widgets.push(*existing); } else { @@ -200,6 +203,16 @@ impl WidgetRects { } } } + + if let Some(shift_start) = shift_layer_index_after { + #[expect(clippy::needless_range_loop)] + for i in shift_start..layer_widgets.len() { + let w = &layer_widgets[i]; + if let Some((idx_in_by_id, _)) = by_id.get_mut(&w.id) { + *idx_in_by_id = i; + } + } + } } pub fn set_info(&mut self, id: Id, info: WidgetInfo) { diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 2d4f79516..fbf25babf 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -1065,51 +1065,73 @@ fn events( .. } => check_for_mutating_key_press(os, &cursor_range, text, galley, modifiers, *key), - Event::Ime(ime_event) => match ime_event { - ImeEvent::Enabled => { - state.ime_enabled = true; - state.ime_cursor_range = cursor_range; - None + Event::Ime(ime_event) => { + /// Empty prediction can be produced with [`ImeEvent::Preedit`] + /// or [`ImeEvent::Commit`] when user press backspace or escape + /// during IME, so this function should be called in both cases + /// to clear current text. + /// + /// Example platforms where only `ImeEvent::Preedit("")` of + /// those two events is emitted when the last character in the + /// prediction is deleted: + /// - macOS 15.7.3. + /// - Debian13 with gnome48 and wayland. + /// + /// An example platform where only `ImeEvent::Commit("")` of + /// those two events is emitted when the last character in the + /// prediction is deleted: + /// - Safari 26.2 (on macOS 15.7.3). + fn clear_prediction( + text: &mut dyn TextBuffer, + cursor_range: &CCursorRange, + ) -> CCursor { + text.delete_selected(cursor_range) } - ImeEvent::Preedit(text_mark) => { - if text_mark == "\n" || text_mark == "\r" { - None - } else { - // Empty prediction can be produced when user press backspace - // or escape during IME, so we clear current text. - let mut ccursor = text.delete_selected(&cursor_range); - let start_cursor = ccursor; - if !text_mark.is_empty() { - text.insert_text_at(&mut ccursor, text_mark, char_limit); - } - state.ime_cursor_range = cursor_range; - Some(CCursorRange::two(start_cursor, ccursor)) - } - } - ImeEvent::Commit(prediction) => { - if prediction == "\n" || prediction == "\r" { - None - } else { - state.ime_enabled = false; - if !prediction.is_empty() - && cursor_range.secondary.index - == state.ime_cursor_range.secondary.index - { - let mut ccursor = text.delete_selected(&cursor_range); - text.insert_text_at(&mut ccursor, prediction, char_limit); - Some(CCursorRange::one(ccursor)) + match ime_event { + ImeEvent::Enabled => { + state.ime_enabled = true; + state.ime_cursor_range = cursor_range; + None + } + ImeEvent::Preedit(text_mark) => { + if text_mark == "\n" || text_mark == "\r" { + None } else { - let ccursor = cursor_range.primary; + let mut ccursor = clear_prediction(text, &cursor_range); + + let start_cursor = ccursor; + if !text_mark.is_empty() { + text.insert_text_at(&mut ccursor, text_mark, char_limit); + } + state.ime_cursor_range = cursor_range; + Some(CCursorRange::two(start_cursor, ccursor)) + } + } + ImeEvent::Commit(prediction) => { + if prediction == "\n" || prediction == "\r" { + None + } else { + state.ime_enabled = false; + + let mut ccursor = clear_prediction(text, &cursor_range); + + if !prediction.is_empty() + && cursor_range.secondary.index + == state.ime_cursor_range.secondary.index + { + text.insert_text_at(&mut ccursor, prediction, char_limit); + } + Some(CCursorRange::one(ccursor)) } } + ImeEvent::Disabled => { + state.ime_enabled = false; + None + } } - ImeEvent::Disabled => { - state.ime_enabled = false; - None - } - }, + } _ => None, }; diff --git a/crates/egui_demo_lib/src/demo/code_editor.rs b/crates/egui_demo_lib/src/demo/code_editor.rs index a8a82e200..869194114 100644 --- a/crates/egui_demo_lib/src/demo/code_editor.rs +++ b/crates/egui_demo_lib/src/demo/code_editor.rs @@ -90,15 +90,25 @@ impl crate::View for CodeEditor { }; egui::ScrollArea::vertical().show(ui, |ui| { - ui.add( - egui::TextEdit::multiline(code) - .font(egui::TextStyle::Monospace) // for cursor height - .code_editor() - .desired_rows(10) - .lock_focus(true) - .desired_width(f32::INFINITY) - .layouter(&mut layouter), - ); + let editor = egui::TextEdit::multiline(code) + .font(egui::TextStyle::Monospace) // for cursor height + .code_editor() + .desired_rows(10) + .lock_focus(true) + .desired_width(f32::INFINITY) + .layouter(&mut layouter); + let editor = if cfg!(feature = "syntect") { + editor + } else { + use egui::Color32; + let background_color = if theme.is_dark() { + Color32::BLACK + } else { + Color32::WHITE + }; + editor.background_color(background_color) + }; + ui.add(editor); }); } } diff --git a/crates/egui_extras/src/syntax_highlighting.rs b/crates/egui_extras/src/syntax_highlighting.rs index 0b9a791ea..adf2c9221 100644 --- a/crates/egui_extras/src/syntax_highlighting.rs +++ b/crates/egui_extras/src/syntax_highlighting.rs @@ -116,6 +116,7 @@ fn highlight_inner( mem.caches .cache::() .get((&font_id, theme, code, language, settings)) + .clone() }) } @@ -228,6 +229,10 @@ impl Default for CodeTheme { } impl CodeTheme { + pub fn is_dark(&self) -> bool { + self.dark_mode + } + /// Selects either dark or light theme based on the given style. pub fn from_style(style: &egui::Style) -> Self { let font_id = style @@ -315,6 +320,24 @@ impl CodeTheme { #[cfg(feature = "syntect")] impl CodeTheme { + /// Change the font size + pub fn with_font_size(&self, font_size: f32) -> Self { + Self { + dark_mode: self.dark_mode, + syntect_theme: self.syntect_theme, + font_id: egui::FontId::monospace(font_size), + } + } + + /// Change the `font_id` of the theme + pub fn with_font_id(&self, font_id: egui::FontId) -> Self { + Self { + dark_mode: self.dark_mode, + syntect_theme: self.syntect_theme, + font_id, + } + } + fn dark_with_font_id(font_id: egui::FontId) -> Self { Self { dark_mode: true, @@ -331,10 +354,6 @@ impl CodeTheme { } } - pub fn is_dark(&self) -> bool { - self.dark_mode - } - /// Show UI for changing the color theme. pub fn ui(&mut self, ui: &mut egui::Ui) { ui.horizontal(|ui| { @@ -344,11 +363,9 @@ impl CodeTheme { ui.selectable_value(&mut self.dark_mode, false, "☀ Light theme") .on_hover_text("Use the light mode theme"); }); - - for theme in SyntectTheme::all() { - if theme.is_dark() == self.dark_mode { - ui.radio_value(&mut self.syntect_theme, theme, theme.name()); - } + let current_theme_is_dark = self.is_dark(); + for theme in SyntectTheme::all().filter(|t| t.is_dark() == current_theme_is_dark) { + ui.radio_value(&mut self.syntect_theme, theme, theme.name()); } } } @@ -408,12 +425,13 @@ impl CodeTheme { ui.vertical(|ui| { ui.set_width(150.0); - egui::widgets::global_theme_preference_buttons(ui); - - ui.add_space(8.0); - ui.separator(); - ui.add_space(8.0); + ui.horizontal(|ui| { + ui.selectable_value(&mut self.dark_mode, true, "🌙 Dark theme") + .on_hover_text("Use the dark mode theme"); + ui.selectable_value(&mut self.dark_mode, false, "☀ Light theme") + .on_hover_text("Use the light mode theme"); + }); ui.scope(|ui| { for (tt, tt_name) in [ (TokenType::Comment, "// comment"), diff --git a/crates/epaint/src/text/font.rs b/crates/epaint/src/text/font.rs index 80fb1245b..150aca34a 100644 --- a/crates/epaint/src/text/font.rs +++ b/crates/epaint/src/text/font.rs @@ -337,9 +337,6 @@ pub struct FontFace { font: FontCell, tweak: FontTweak, - /// The font weight (100-900) if available from the font file. - weight: Option, - /// Variable font location (for weight axis, etc.) location: skrifa::instance::Location, glyph_info_cache: ahash::HashMap, @@ -436,18 +433,12 @@ impl FontFace { name, font, tweak, - weight, location, glyph_info_cache: Default::default(), glyph_alloc_cache: Default::default(), }) } - /// Get the font weight (100-900) if available from the font file. - pub fn weight(&self) -> Option { - self.weight - } - /// Code points that will always be replaced by the replacement character. /// /// See also [`invisible_char`]. diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 8ca78064d..19876b571 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -861,26 +861,6 @@ impl FontsImpl { atlas: &mut self.atlas, } } - - /// Get the weight of a font by name, if available. - /// - /// Returns the weight value (100-900) read from the font file's OS/2 table, - /// or `None` if the font is not found or doesn't contain weight information. - /// - /// # Example - /// ``` - /// # use epaint::text::{FontDefinitions, FontsImpl}; - /// # use epaint::TextOptions; - /// let fonts_impl = FontsImpl::new(TextOptions::default(), FontDefinitions::default()); - /// if let Some(weight) = fonts_impl.font_weight("Hack") { - /// println!("Hack font weight: {}", weight); - /// } - /// ``` - pub fn font_weight(&self, font_name: &str) -> Option { - let key = self.fonts_by_name.get(font_name)?; - let font_face = self.fonts_by_id.get(key)?; - font_face.weight() - } } // ----------------------------------------------------------------------------