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

Merge branch 'emilk:main' into main

This commit is contained in:
AdrienZ.
2026-01-14 04:04:21 +01:00
committed by GitHub
25 changed files with 342 additions and 172 deletions

View File

@@ -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

View File

@@ -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 <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();
}
}
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<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) {
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")]

View File

@@ -19,7 +19,7 @@ use super::CacheTrait;
///
/// # let mut cache_storage = CacheStorage::default();
/// let mut cache = cache_storage.cache::<CharCountCache<'_>>();
/// 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<Cache: CacheTrait + Default>(&mut self) -> &mut Cache {
#[expect(clippy::unwrap_used)]
self.caches
let cache = self
.caches
.entry(std::any::TypeId::of::<Cache>())
.or_insert_with(|| Box::<Cache>::default())
.as_any_mut()
.or_insert_with(|| Box::<Cache>::default());
#[expect(clippy::unwrap_used)]
(cache.as_mut() as &mut dyn std::any::Any)
.downcast_mut::<Cache>()
.unwrap()
}

View File

@@ -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;
}

View File

@@ -46,10 +46,9 @@ impl<Value, Computer> FrameCache<Value, Computer> {
impl<Value, Computer> FrameCache<Value, Computer> {
/// Get from cache (if the same key was used last frame)
/// 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
Key: Copy + std::hash::Hash,
Value: Clone,
Computer: ComputerMut<Key, Value>,
{
let hash = crate::util::hash(key);
@@ -58,12 +57,12 @@ impl<Value, Computer> FrameCache<Value, Computer> {
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<Value: 'static + Send + Sync, Computer: 'static + Send + Sync> CacheTrait
fn len(&self) -> usize {
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 {
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 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()
}

View File

@@ -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()
};

View File

@@ -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
{

View File

@@ -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());
}

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`].
///
/// 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.

View File

@@ -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<Pos2>,
/// 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:#?}"));

View File

@@ -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;

View File

@@ -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`].

View File

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

View File

@@ -55,6 +55,9 @@ pub(crate) struct PluginHandle {
plugin: Box<dyn Plugin>,
}
/// A typed handle to a registered [`Plugin`].
///
/// Use [`Self::lock`] to access the plugin.
pub struct TypedPluginHandle<P: Plugin> {
handle: Arc<Mutex<PluginHandle>>,
_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> {
TypedPluginGuard {
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> {
guard: MutexGuard<'a, PluginHandle>,
_type: std::marker::PhantomData<P>,
@@ -113,13 +120,13 @@ impl PluginHandle {
}
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>()
.expect("PluginHandle: plugin is not of the expected type")
}
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>()
.expect("PluginHandle: plugin is not of the expected type")
}

View File

@@ -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

View File

@@ -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"));
}
}

View File

@@ -332,6 +332,7 @@ pub struct ViewportBuilder {
// X11
pub window_type: Option<X11WindowType>,
pub override_redirect: Option<bool>,
}
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)
}
}

View File

@@ -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) {

View File

@@ -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,
};

View File

@@ -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);
});
}
}

View File

@@ -116,6 +116,7 @@ fn highlight_inner(
mem.caches
.cache::<HighlightCache>()
.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"),

View File

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

View File

@@ -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<u16> {
let key = self.fonts_by_name.get(font_name)?;
let font_face = self.fonts_by_id.get(key)?;
font_face.weight()
}
}
// ----------------------------------------------------------------------------