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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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")]
|
||||||
|
|||||||
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_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()
|
||||||
}
|
}
|
||||||
|
|||||||
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.
|
/// 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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
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> {
|
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
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 {
|
fn len(&self) -> usize {
|
||||||
self.cache.len()
|
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 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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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:#?}"));
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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`].
|
||||||
|
|||||||
@@ -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))]
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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`].
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user