1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-31 13:50:04 -04:00

Merge branch 'main' of github.com:AdrienZianne/egui

This commit is contained in:
adrien
2026-01-14 04:04:53 +01:00
25 changed files with 342 additions and 172 deletions

View File

@@ -14,6 +14,7 @@ jobs:
update-snapshots: update-snapshots:
name: Update snapshots from artifact name: Update snapshots from artifact
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.ref_name != 'main' # We never want to update snapshots directly on main
permissions: permissions:
contents: write contents: write

View File

@@ -352,45 +352,7 @@ impl State {
} }
WindowEvent::Ime(ime) => { WindowEvent::Ime(ime) => {
// on Mac even Cmd-C is pressed during ime, a `c` is pushed to Preedit. self.on_ime(ime);
// 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 <https://github.com/rust-windowing/winit/issues/2498>
} 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();
}
}
EventResponse { EventResponse {
repaint: true, 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<Option<winit::event::Ime>> =
// 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 <https://github.com/rust-windowing/winit/issues/2498>
} 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) { pub fn ime_event_enable(&mut self) {
if !self.has_sent_ime_enabled { if !self.has_sent_ime_enabled {
self.egui_input self.egui_input
@@ -1668,6 +1728,7 @@ pub fn create_winit_window_attributes(
// x11 // x11
window_type: _window_type, window_type: _window_type,
override_redirect: _override_redirect,
mouse_passthrough: _, // handled in `apply_viewport_builder_to_window` mouse_passthrough: _, // handled in `apply_viewport_builder_to_window`
clamp_size_to_monitor_size: _, // Handled in `viewport_builder` in `epi_integration.rs` 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"))] #[cfg(all(feature = "x11", target_os = "linux"))]
{ {
use winit::platform::x11::WindowAttributesExtX11 as _;
if let Some(window_type) = _window_type { if let Some(window_type) = _window_type {
use winit::platform::x11::WindowAttributesExtX11 as _;
use winit::platform::x11::WindowType; use winit::platform::x11::WindowType;
window_attributes = window_attributes.with_x11_window_type(vec![match window_type { window_attributes = window_attributes.with_x11_window_type(vec![match window_type {
egui::X11WindowType::Normal => WindowType::Normal, egui::X11WindowType::Normal => WindowType::Normal,
@@ -1787,6 +1848,9 @@ pub fn create_winit_window_attributes(
egui::X11WindowType::Dnd => WindowType::Dnd, 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")] #[cfg(target_os = "windows")]

View File

@@ -19,7 +19,7 @@ use super::CacheTrait;
/// ///
/// # let mut cache_storage = CacheStorage::default(); /// # let mut cache_storage = CacheStorage::default();
/// let mut cache = cache_storage.cache::<CharCountCache<'_>>(); /// let mut cache = cache_storage.cache::<CharCountCache<'_>>();
/// assert_eq!(cache.get("hello"), 5); /// assert_eq!(*cache.get("hello"), 5);
/// ``` /// ```
#[derive(Default)] #[derive(Default)]
pub struct CacheStorage { pub struct CacheStorage {
@@ -28,11 +28,13 @@ pub struct CacheStorage {
impl CacheStorage { impl CacheStorage {
pub fn cache<Cache: CacheTrait + Default>(&mut self) -> &mut Cache { pub fn cache<Cache: CacheTrait + Default>(&mut self) -> &mut Cache {
#[expect(clippy::unwrap_used)] let cache = self
self.caches .caches
.entry(std::any::TypeId::of::<Cache>()) .entry(std::any::TypeId::of::<Cache>())
.or_insert_with(|| Box::<Cache>::default()) .or_insert_with(|| Box::<Cache>::default());
.as_any_mut()
#[expect(clippy::unwrap_used)]
(cache.as_mut() as &mut dyn std::any::Any)
.downcast_mut::<Cache>() .downcast_mut::<Cache>()
.unwrap() .unwrap()
} }

View File

@@ -1,11 +1,9 @@
/// A cache, storing some value for some length of time. /// A cache, storing some value for some length of time.
#[expect(clippy::len_without_is_empty)] #[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. /// Call once per frame to evict cache.
fn update(&mut self); fn update(&mut self);
/// Number of values currently in the cache. /// Number of values currently in the cache.
fn len(&self) -> usize; fn len(&self) -> usize;
fn as_any_mut(&mut self) -> &mut dyn std::any::Any;
} }

View File

@@ -46,10 +46,9 @@ impl<Value, Computer> FrameCache<Value, Computer> {
impl<Value, Computer> FrameCache<Value, Computer> { impl<Value, Computer> FrameCache<Value, Computer> {
/// Get from cache (if the same key was used last frame) /// Get from cache (if the same key was used last frame)
/// or recompute and store in the cache. /// or recompute and store in the cache.
pub fn get<Key>(&mut self, key: Key) -> Value pub fn get<Key>(&mut self, key: Key) -> &Value
where where
Key: Copy + std::hash::Hash, Key: Copy + std::hash::Hash,
Value: Clone,
Computer: ComputerMut<Key, Value>, Computer: ComputerMut<Key, Value>,
{ {
let hash = crate::util::hash(key); let hash = crate::util::hash(key);
@@ -58,12 +57,12 @@ impl<Value, Computer> FrameCache<Value, Computer> {
std::collections::hash_map::Entry::Occupied(entry) => { std::collections::hash_map::Entry::Occupied(entry) => {
let cached = entry.into_mut(); let cached = entry.into_mut();
cached.0 = self.generation; cached.0 = self.generation;
cached.1.clone() &cached.1
} }
std::collections::hash_map::Entry::Vacant(entry) => { std::collections::hash_map::Entry::Vacant(entry) => {
let value = self.computer.compute(key); let value = self.computer.compute(key);
entry.insert((self.generation, value.clone())); let inserted = entry.insert((self.generation, value));
value &inserted.1
} }
} }
} }
@@ -79,8 +78,4 @@ impl<Value: 'static + Send + Sync, Computer: 'static + Send + Sync> CacheTrait
fn len(&self) -> usize { fn len(&self) -> usize {
self.cache.len() self.cache.len()
} }
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
} }

View File

@@ -54,8 +54,4 @@ where
fn len(&self) -> usize { fn len(&self) -> usize {
self.cache.len() self.cache.len()
} }
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
} }

View File

@@ -505,9 +505,9 @@ impl Area {
let interact_id = layer_id.id.with("move"); let interact_id = layer_id.id.with("move");
let sense = sense.unwrap_or_else(|| { let sense = sense.unwrap_or_else(|| {
if movable { if movable {
Sense::drag() Sense::DRAG
} else if interactable { } else if interactable {
Sense::click() // allow clicks to bring to front Sense::CLICK // allow clicks to bring to front
} else { } else {
Sense::hover() Sense::hover()
} }

View File

@@ -810,7 +810,7 @@ impl ScrollArea {
// or we will steal input from the widgets we contain. // or we will steal input from the widgets we contain.
let content_response_option = state let content_response_option = state
.interact_rect .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 if content_response_option
.as_ref() .as_ref()
@@ -1276,7 +1276,7 @@ impl Prepared {
}; };
let sense = if scroll_source.scroll_bar && ui.is_enabled() { let sense = if scroll_source.scroll_bar && ui.is_enabled() {
Sense::click_and_drag() Sense::CLICK | Sense::DRAG
} else { } else {
Sense::hover() Sense::hover()
}; };

View File

@@ -937,7 +937,7 @@ fn move_and_resize_window(ctx: &Context, id: Id, interaction: &ResizeInteraction
fn do_resize_interaction( fn do_resize_interaction(
ctx: &Context, ctx: &Context,
possible: PossibleInteractions, possible: PossibleInteractions,
_accessibility_parent: Id, accessibility_parent: Id,
layer_id: LayerId, layer_id: LayerId,
outer_rect: Rect, outer_rect: Rect,
window_frame: Frame, window_frame: Frame,
@@ -957,14 +957,14 @@ fn do_resize_interaction(
let rect = outer_rect.shrink(window_frame.stroke.width / 2.0); let rect = outer_rect.shrink(window_frame.stroke.width / 2.0);
let side_response = |rect, id| { let side_response = |rect, id| {
ctx.register_accesskit_parent(id, _accessibility_parent); ctx.register_accesskit_parent(id, accessibility_parent);
let response = ctx.create_widget( let response = ctx.create_widget(
WidgetRect { WidgetRect {
layer_id, layer_id,
id, id,
rect, rect,
interact_rect: 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, enabled: true,
}, },
true, true,
@@ -1324,7 +1324,7 @@ impl TitleBar {
let id = ui.unique_id().with("__window_title_bar"); let id = ui.unique_id().with("__window_title_bar");
if ui if ui
.interact(double_click_rect, id, Sense::click()) .interact(double_click_rect, id, Sense::CLICK)
.double_clicked() .double_clicked()
&& collapsible && collapsible
{ {

View File

@@ -2432,7 +2432,8 @@ impl Context {
if let Some(widget) = if let Some(widget) =
self.write(|ctx| ctx.viewport().this_pass.widgets.get(id).copied()) 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()) { if let Some(debug_rect) = self.pass_state_mut(|fs| fs.debug_rect.take()) {
debug_rect.paint(&self.debug_painter()); debug_rect.paint(&self.debug_painter());
} }

View File

@@ -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`]. /// 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 /// NOTE: For cross-platform uses, ALT+SHIFT is a bad combination of modifiers
/// as on mac that is how you type special characters, /// as on mac that is how you type special characters,
/// so those key presses are usually not reported to egui. /// so those key presses are usually not reported to egui.

View File

@@ -970,6 +970,15 @@ impl PointerEvent {
} }
/// Mouse or touch state. /// 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)] #[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct PointerState { pub struct PointerState {
@@ -1028,6 +1037,10 @@ pub struct PointerState {
/// This could also be the trigger point for a long-touch. /// This could also be the trigger point for a long-touch.
pub(crate) started_decidedly_dragging: bool, pub(crate) started_decidedly_dragging: bool,
/// Where did the last click originate?
/// `None` if no mouse click occurred.
last_click_pos: Option<Pos2>,
/// When did the pointer get click last? /// When did the pointer get click last?
/// Used to check for double-clicks. /// Used to check for double-clicks.
last_click_time: f64, last_click_time: f64,
@@ -1065,6 +1078,7 @@ impl Default for PointerState {
press_start_time: None, press_start_time: None,
has_moved_too_much_for_a_click: false, has_moved_too_much_for_a_click: false,
started_decidedly_dragging: false, started_decidedly_dragging: false,
last_click_pos: None,
last_click_time: f64::NEG_INFINITY, last_click_time: f64::NEG_INFINITY,
last_last_click_time: f64::NEG_INFINITY, last_last_click_time: f64::NEG_INFINITY,
last_move_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 clicked = self.could_any_button_be_click();
let click = if clicked { let click = if clicked {
let double_click = let click_dist_sq = self
(time - self.last_click_time) < self.options.max_double_click_delay; .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) 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 { let count = if triple_click {
3 3
} else if double_click { } else if double_click {
@@ -1154,6 +1176,7 @@ impl PointerState {
self.last_last_click_time = self.last_click_time; self.last_last_click_time = self.last_click_time;
self.last_click_time = time; self.last_click_time = time;
self.last_click_pos = Some(pos);
Some(Click { Some(Click {
pos, pos,
@@ -1621,6 +1644,7 @@ impl PointerState {
press_start_time, press_start_time,
has_moved_too_much_for_a_click, has_moved_too_much_for_a_click,
started_decidedly_dragging, started_decidedly_dragging,
last_click_pos,
last_click_time, last_click_time,
last_last_click_time, last_last_click_time,
pointer_events, pointer_events,
@@ -1646,6 +1670,7 @@ impl PointerState {
ui.label(format!( ui.label(format!(
"started_decidedly_dragging: {started_decidedly_dragging}" "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_click_time: {last_click_time:#?}"));
ui.label(format!("last_last_click_time: {last_last_click_time:#?}")); ui.label(format!("last_last_click_time: {last_last_click_time:#?}"));
ui.label(format!("last_move_time: {last_move_time:#?}")); ui.label(format!("last_move_time: {last_move_time:#?}"));

View File

@@ -413,7 +413,7 @@ pub mod os;
mod painter; mod painter;
mod pass_state; mod pass_state;
pub(crate) mod placer; pub(crate) mod placer;
mod plugin; pub mod plugin;
pub mod response; pub mod response;
mod sense; mod sense;
pub mod style; pub mod style;

View File

@@ -3,7 +3,7 @@
//! If you just want to display some images, [`egui_extras`](https://crates.io/crates/egui_extras/) //! 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. //! 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) //! 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. //! in your app's setup code.
//! 3. Use [`Ui::image`][`crate::ui::Ui::image`] with some [`ImageSource`][`crate::ImageSource`]. //! 3. Use [`Ui::image`][`crate::ui::Ui::image`] with some [`ImageSource`][`crate::ImageSource`].

View File

@@ -68,7 +68,7 @@ pub struct Memory {
/// # let mut ctx = egui::Context::default(); /// # let mut ctx = egui::Context::default();
/// ctx.memory_mut(|mem| { /// ctx.memory_mut(|mem| {
/// let cache = mem.caches.cache::<CharCountCache<'_>>(); /// let cache = mem.caches.cache::<CharCountCache<'_>>();
/// assert_eq!(cache.get("hello"), 5); /// assert_eq!(*cache.get("hello"), 5);
/// }); /// });
/// ``` /// ```
#[cfg_attr(feature = "persistence", serde(skip))] #[cfg_attr(feature = "persistence", serde(skip))]

View File

@@ -55,6 +55,9 @@ pub(crate) struct PluginHandle {
plugin: Box<dyn Plugin>, plugin: Box<dyn Plugin>,
} }
/// A typed handle to a registered [`Plugin`].
///
/// Use [`Self::lock`] to access the plugin.
pub struct TypedPluginHandle<P: Plugin> { pub struct TypedPluginHandle<P: Plugin> {
handle: Arc<Mutex<PluginHandle>>, handle: Arc<Mutex<PluginHandle>>,
_type: std::marker::PhantomData<P>, _type: std::marker::PhantomData<P>,
@@ -68,6 +71,9 @@ impl<P: Plugin> TypedPluginHandle<P> {
} }
} }
/// Lock the plugin for access.
///
/// Returns a guard that dereferences to the plugin.
pub fn lock(&self) -> TypedPluginGuard<'_, P> { pub fn lock(&self) -> TypedPluginGuard<'_, P> {
TypedPluginGuard { TypedPluginGuard {
guard: self.handle.lock(), guard: self.handle.lock(),
@@ -76,6 +82,7 @@ impl<P: Plugin> TypedPluginHandle<P> {
} }
} }
/// A guard that provides access to a [`Plugin`].
pub struct TypedPluginGuard<'a, P: Plugin> { pub struct TypedPluginGuard<'a, P: Plugin> {
guard: MutexGuard<'a, PluginHandle>, guard: MutexGuard<'a, PluginHandle>,
_type: std::marker::PhantomData<P>, _type: std::marker::PhantomData<P>,
@@ -113,13 +120,13 @@ impl PluginHandle {
} }
fn typed_plugin<P: Plugin + 'static>(&self) -> &P { fn typed_plugin<P: Plugin + 'static>(&self) -> &P {
(&*self.plugin as &dyn std::any::Any) (self.plugin.as_ref() as &dyn std::any::Any)
.downcast_ref::<P>() .downcast_ref::<P>()
.expect("PluginHandle: plugin is not of the expected type") .expect("PluginHandle: plugin is not of the expected type")
} }
pub fn typed_plugin_mut<P: Plugin + 'static>(&mut self) -> &mut P { pub fn typed_plugin_mut<P: Plugin + 'static>(&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::<P>() .downcast_mut::<P>()
.expect("PluginHandle: plugin is not of the expected type") .expect("PluginHandle: plugin is not of the expected type")
} }

View File

@@ -53,19 +53,23 @@ impl Sense {
Self::FOCUSABLE 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] #[inline]
pub fn click() -> Self { pub fn click() -> Self {
Self::CLICK | Self::FOCUSABLE 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] #[inline]
pub fn drag() -> Self { pub fn drag() -> Self {
Self::DRAG | Self::FOCUSABLE 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, /// 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 /// because when the user starts a press egui can't know if this is the start

View File

@@ -1303,6 +1303,13 @@ pub struct DebugOptions {
/// ///
/// See [`emath::GuiRounding`] for more. /// See [`emath::GuiRounding`] for more.
pub show_unaligned: bool, 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)] #[cfg(debug_assertions)]
@@ -1319,6 +1326,7 @@ impl Default for DebugOptions {
show_interactive_widgets: false, show_interactive_widgets: false,
show_widget_hits: false, show_widget_hits: false,
show_unaligned: cfg!(debug_assertions), show_unaligned: cfg!(debug_assertions),
show_focused_widget: false,
} }
} }
} }
@@ -2480,6 +2488,7 @@ impl DebugOptions {
show_interactive_widgets, show_interactive_widgets,
show_widget_hits, show_widget_hits,
show_unaligned, show_unaligned,
show_focused_widget,
} = self; } = self;
{ {
@@ -2514,6 +2523,11 @@ impl DebugOptions {
"Show rectangles not aligned to integer point coordinates", "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")); ui.vertical_centered(|ui| reset_button(ui, self, "Reset debug options"));
} }
} }

View File

@@ -332,6 +332,7 @@ pub struct ViewportBuilder {
// X11 // X11
pub window_type: Option<X11WindowType>, pub window_type: Option<X11WindowType>,
pub override_redirect: Option<bool>,
} }
impl ViewportBuilder { impl ViewportBuilder {
@@ -663,13 +664,22 @@ impl ViewportBuilder {
/// ### On X11 /// ### On X11
/// This sets the window type. /// 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] #[inline]
pub fn with_window_type(mut self, value: X11WindowType) -> Self { pub fn with_window_type(mut self, value: X11WindowType) -> Self {
self.window_type = Some(value); self.window_type = Some(value);
self 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, /// Update this `ViewportBuilder` with a delta,
/// returning a list of commands and a bool indicating if the window needs to be recreated. /// returning a list of commands and a bool indicating if the window needs to be recreated.
#[must_use] #[must_use]
@@ -706,6 +716,7 @@ impl ViewportBuilder {
mouse_passthrough: new_mouse_passthrough, mouse_passthrough: new_mouse_passthrough,
taskbar: new_taskbar, taskbar: new_taskbar,
window_type: new_window_type, window_type: new_window_type,
override_redirect: new_override_redirect,
} = new_vp_builder; } = new_vp_builder;
let mut commands = Vec::new(); let mut commands = Vec::new();
@@ -903,6 +914,11 @@ impl ViewportBuilder {
recreate_window = true; 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) (commands, recreate_window)
} }
} }

View File

@@ -164,6 +164,8 @@ impl WidgetRects {
let InteractOptions { move_to_top } = options; let InteractOptions { move_to_top } = options;
let mut shift_layer_index_after = None;
let layer_widgets = by_layer.entry(layer_id).or_default(); let layer_widgets = by_layer.entry(layer_id).or_default();
match by_id.entry(widget_rect.id) { match by_id.entry(widget_rect.id) {
@@ -187,6 +189,7 @@ impl WidgetRects {
if existing.layer_id == widget_rect.layer_id { if existing.layer_id == widget_rect.layer_id {
if move_to_top { if move_to_top {
layer_widgets.remove(*idx_in_layer); layer_widgets.remove(*idx_in_layer);
shift_layer_index_after = Some(*idx_in_layer);
*idx_in_layer = layer_widgets.len(); *idx_in_layer = layer_widgets.len();
layer_widgets.push(*existing); layer_widgets.push(*existing);
} else { } 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) { pub fn set_info(&mut self, id: Id, info: WidgetInfo) {

View File

@@ -1065,51 +1065,73 @@ fn events(
.. ..
} => check_for_mutating_key_press(os, &cursor_range, text, galley, modifiers, *key), } => check_for_mutating_key_press(os, &cursor_range, text, galley, modifiers, *key),
Event::Ime(ime_event) => match ime_event { Event::Ime(ime_event) => {
ImeEvent::Enabled => { /// Empty prediction can be produced with [`ImeEvent::Preedit`]
state.ime_enabled = true; /// or [`ImeEvent::Commit`] when user press backspace or escape
state.ime_cursor_range = cursor_range; /// during IME, so this function should be called in both cases
None /// 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() match ime_event {
&& cursor_range.secondary.index ImeEvent::Enabled => {
== state.ime_cursor_range.secondary.index state.ime_enabled = true;
{ state.ime_cursor_range = cursor_range;
let mut ccursor = text.delete_selected(&cursor_range); None
text.insert_text_at(&mut ccursor, prediction, char_limit); }
Some(CCursorRange::one(ccursor)) ImeEvent::Preedit(text_mark) => {
if text_mark == "\n" || text_mark == "\r" {
None
} else { } 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)) Some(CCursorRange::one(ccursor))
} }
} }
ImeEvent::Disabled => {
state.ime_enabled = false;
None
}
} }
ImeEvent::Disabled => { }
state.ime_enabled = false;
None
}
},
_ => None, _ => None,
}; };

View File

@@ -90,15 +90,25 @@ impl crate::View for CodeEditor {
}; };
egui::ScrollArea::vertical().show(ui, |ui| { egui::ScrollArea::vertical().show(ui, |ui| {
ui.add( let editor = egui::TextEdit::multiline(code)
egui::TextEdit::multiline(code) .font(egui::TextStyle::Monospace) // for cursor height
.font(egui::TextStyle::Monospace) // for cursor height .code_editor()
.code_editor() .desired_rows(10)
.desired_rows(10) .lock_focus(true)
.lock_focus(true) .desired_width(f32::INFINITY)
.desired_width(f32::INFINITY) .layouter(&mut layouter);
.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);
}); });
} }
} }

View File

@@ -116,6 +116,7 @@ fn highlight_inner(
mem.caches mem.caches
.cache::<HighlightCache>() .cache::<HighlightCache>()
.get((&font_id, theme, code, language, settings)) .get((&font_id, theme, code, language, settings))
.clone()
}) })
} }
@@ -228,6 +229,10 @@ impl Default for CodeTheme {
} }
impl CodeTheme { impl CodeTheme {
pub fn is_dark(&self) -> bool {
self.dark_mode
}
/// Selects either dark or light theme based on the given style. /// Selects either dark or light theme based on the given style.
pub fn from_style(style: &egui::Style) -> Self { pub fn from_style(style: &egui::Style) -> Self {
let font_id = style let font_id = style
@@ -315,6 +320,24 @@ impl CodeTheme {
#[cfg(feature = "syntect")] #[cfg(feature = "syntect")]
impl CodeTheme { 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 { fn dark_with_font_id(font_id: egui::FontId) -> Self {
Self { Self {
dark_mode: true, 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. /// Show UI for changing the color theme.
pub fn ui(&mut self, ui: &mut egui::Ui) { pub fn ui(&mut self, ui: &mut egui::Ui) {
ui.horizontal(|ui| { ui.horizontal(|ui| {
@@ -344,11 +363,9 @@ impl CodeTheme {
ui.selectable_value(&mut self.dark_mode, false, "☀ Light theme") ui.selectable_value(&mut self.dark_mode, false, "☀ Light theme")
.on_hover_text("Use the light mode theme"); .on_hover_text("Use the light mode theme");
}); });
let current_theme_is_dark = self.is_dark();
for theme in SyntectTheme::all() { for theme in SyntectTheme::all().filter(|t| t.is_dark() == current_theme_is_dark) {
if theme.is_dark() == self.dark_mode { ui.radio_value(&mut self.syntect_theme, theme, theme.name());
ui.radio_value(&mut self.syntect_theme, theme, theme.name());
}
} }
} }
} }
@@ -408,12 +425,13 @@ impl CodeTheme {
ui.vertical(|ui| { ui.vertical(|ui| {
ui.set_width(150.0); ui.set_width(150.0);
egui::widgets::global_theme_preference_buttons(ui); ui.horizontal(|ui| {
ui.selectable_value(&mut self.dark_mode, true, "🌙 Dark theme")
ui.add_space(8.0); .on_hover_text("Use the dark mode theme");
ui.separator();
ui.add_space(8.0);
ui.selectable_value(&mut self.dark_mode, false, "☀ Light theme")
.on_hover_text("Use the light mode theme");
});
ui.scope(|ui| { ui.scope(|ui| {
for (tt, tt_name) in [ for (tt, tt_name) in [
(TokenType::Comment, "// comment"), (TokenType::Comment, "// comment"),

View File

@@ -337,9 +337,6 @@ pub struct FontFace {
font: FontCell, font: FontCell,
tweak: FontTweak, tweak: FontTweak,
/// The font weight (100-900) if available from the font file.
weight: Option<u16>,
/// Variable font location (for weight axis, etc.) /// Variable font location (for weight axis, etc.)
location: skrifa::instance::Location, location: skrifa::instance::Location,
glyph_info_cache: ahash::HashMap<char, GlyphInfo>, glyph_info_cache: ahash::HashMap<char, GlyphInfo>,
@@ -436,18 +433,12 @@ impl FontFace {
name, name,
font, font,
tweak, tweak,
weight,
location, location,
glyph_info_cache: Default::default(), glyph_info_cache: Default::default(),
glyph_alloc_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<u16> {
self.weight
}
/// Code points that will always be replaced by the replacement character. /// Code points that will always be replaced by the replacement character.
/// ///
/// See also [`invisible_char`]. /// See also [`invisible_char`].

View File

@@ -861,26 +861,6 @@ impl FontsImpl {
atlas: &mut self.atlas, 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<u16> {
let key = self.fonts_by_name.get(font_name)?;
let font_face = self.fonts_by_id.get(key)?;
font_face.weight()
}
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------