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:
@@ -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
|
||||
|
||||
|
||||
@@ -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")]
|
||||
|
||||
12
crates/egui/src/cache/cache_storage.rs
vendored
12
crates/egui/src/cache/cache_storage.rs
vendored
@@ -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()
|
||||
}
|
||||
|
||||
4
crates/egui/src/cache/cache_trait.rs
vendored
4
crates/egui/src/cache/cache_trait.rs
vendored
@@ -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;
|
||||
}
|
||||
|
||||
13
crates/egui/src/cache/frame_cache.rs
vendored
13
crates/egui/src/cache/frame_cache.rs
vendored
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
4
crates/egui/src/cache/frame_publisher.rs
vendored
4
crates/egui/src/cache/frame_publisher.rs
vendored
@@ -54,8 +54,4 @@ where
|
||||
fn len(&self) -> usize {
|
||||
self.cache.len()
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
};
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:#?}"));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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`].
|
||||
|
||||
@@ -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))]
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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`].
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user