mirror of
https://github.com/emilk/egui.git
synced 2026-06-26 14:49:06 -04:00
Merge branch 'main' into ime-preedit-visuals
# Conflicts: # crates/egui/src/data/input.rs
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
|
||||
[default.extend-words]
|
||||
ime = "ime" # Input Method Editor
|
||||
abou = "abou" # part of @AmmarAbouZor username
|
||||
nknown = "nknown" # part of @55nknown username
|
||||
isse = "isse" # part of @IsseW username
|
||||
tye = "tye" # part of @tye-exe username
|
||||
|
||||
@@ -14,6 +14,10 @@ This file is updated upon each release.
|
||||
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
|
||||
|
||||
|
||||
## 0.34.3 - 2026-05-27
|
||||
* Fix `ScrollArea::scroll_to_*` calls when `stick_to_bottom` is Active [#8033](https://github.com/emilk/egui/pull/8033) by [@AmmarAbouZor](https://github.com/AmmarAbouZor)
|
||||
|
||||
|
||||
## 0.34.2 - 2026-05-04
|
||||
### ⭐ Added
|
||||
* Add regression test for O(n²) word boundary scan [#8077](https://github.com/emilk/egui/pull/8077) by [@hallyhaa](https://github.com/hallyhaa)
|
||||
|
||||
32
Cargo.lock
32
Cargo.lock
@@ -1197,7 +1197,7 @@ checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53"
|
||||
|
||||
[[package]]
|
||||
name = "ecolor"
|
||||
version = "0.34.2"
|
||||
version = "0.34.3"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"cint",
|
||||
@@ -1209,7 +1209,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "eframe"
|
||||
version = "0.34.2"
|
||||
version = "0.34.3"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"bytemuck",
|
||||
@@ -1248,7 +1248,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "egui"
|
||||
version = "0.34.2"
|
||||
version = "0.34.3"
|
||||
dependencies = [
|
||||
"accesskit",
|
||||
"ahash",
|
||||
@@ -1269,7 +1269,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "egui-wgpu"
|
||||
version = "0.34.2"
|
||||
version = "0.34.3"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"bytemuck",
|
||||
@@ -1287,7 +1287,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "egui-winit"
|
||||
version = "0.34.2"
|
||||
version = "0.34.3"
|
||||
dependencies = [
|
||||
"accesskit_winit",
|
||||
"arboard",
|
||||
@@ -1310,7 +1310,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "egui_demo_app"
|
||||
version = "0.34.2"
|
||||
version = "0.34.3"
|
||||
dependencies = [
|
||||
"accesskit",
|
||||
"accesskit_consumer",
|
||||
@@ -1339,7 +1339,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "egui_demo_lib"
|
||||
version = "0.34.2"
|
||||
version = "0.34.3"
|
||||
dependencies = [
|
||||
"criterion",
|
||||
"document-features",
|
||||
@@ -1356,7 +1356,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "egui_extras"
|
||||
version = "0.34.2"
|
||||
version = "0.34.3"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"document-features",
|
||||
@@ -1376,7 +1376,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "egui_glow"
|
||||
version = "0.34.2"
|
||||
version = "0.34.3"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"document-features",
|
||||
@@ -1393,7 +1393,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "egui_kittest"
|
||||
version = "0.34.2"
|
||||
version = "0.34.3"
|
||||
dependencies = [
|
||||
"dify",
|
||||
"document-features",
|
||||
@@ -1413,7 +1413,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "egui_tests"
|
||||
version = "0.34.2"
|
||||
version = "0.34.3"
|
||||
dependencies = [
|
||||
"egui",
|
||||
"egui_extras",
|
||||
@@ -1443,7 +1443,7 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
|
||||
|
||||
[[package]]
|
||||
name = "emath"
|
||||
version = "0.34.2"
|
||||
version = "0.34.3"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"document-features",
|
||||
@@ -1541,7 +1541,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "epaint"
|
||||
version = "0.34.2"
|
||||
version = "0.34.3"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"bytemuck",
|
||||
@@ -1570,7 +1570,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "epaint_default_fonts"
|
||||
version = "0.34.2"
|
||||
version = "0.34.3"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
@@ -3545,7 +3545,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "popups"
|
||||
version = "0.34.2"
|
||||
version = "0.34.3"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"env_logger",
|
||||
@@ -5893,7 +5893,7 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.34.2"
|
||||
version = "0.34.3"
|
||||
|
||||
[[package]]
|
||||
name = "yaml-rust"
|
||||
|
||||
26
Cargo.toml
26
Cargo.toml
@@ -24,7 +24,7 @@ members = [
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
rust-version = "1.92"
|
||||
version = "0.34.2"
|
||||
version = "0.34.3"
|
||||
|
||||
|
||||
[profile.release]
|
||||
@@ -55,18 +55,18 @@ opt-level = 2
|
||||
|
||||
|
||||
[workspace.dependencies]
|
||||
emath = { version = "0.34.2", path = "crates/emath", default-features = false }
|
||||
ecolor = { version = "0.34.2", path = "crates/ecolor", default-features = false }
|
||||
epaint = { version = "0.34.2", path = "crates/epaint", default-features = false }
|
||||
epaint_default_fonts = { version = "0.34.2", path = "crates/epaint_default_fonts" }
|
||||
egui = { version = "0.34.2", path = "crates/egui", default-features = false }
|
||||
egui-winit = { version = "0.34.2", path = "crates/egui-winit", default-features = false }
|
||||
egui_extras = { version = "0.34.2", path = "crates/egui_extras", default-features = false }
|
||||
egui-wgpu = { version = "0.34.2", path = "crates/egui-wgpu", default-features = false }
|
||||
egui_demo_lib = { version = "0.34.2", path = "crates/egui_demo_lib", default-features = false }
|
||||
egui_glow = { version = "0.34.2", path = "crates/egui_glow", default-features = false }
|
||||
egui_kittest = { version = "0.34.2", path = "crates/egui_kittest", default-features = false }
|
||||
eframe = { version = "0.34.2", path = "crates/eframe", default-features = false }
|
||||
emath = { version = "0.34.3", path = "crates/emath", default-features = false }
|
||||
ecolor = { version = "0.34.3", path = "crates/ecolor", default-features = false }
|
||||
epaint = { version = "0.34.3", path = "crates/epaint", default-features = false }
|
||||
epaint_default_fonts = { version = "0.34.3", path = "crates/epaint_default_fonts" }
|
||||
egui = { version = "0.34.3", path = "crates/egui", default-features = false }
|
||||
egui-winit = { version = "0.34.3", path = "crates/egui-winit", default-features = false }
|
||||
egui_extras = { version = "0.34.3", path = "crates/egui_extras", default-features = false }
|
||||
egui-wgpu = { version = "0.34.3", path = "crates/egui-wgpu", default-features = false }
|
||||
egui_demo_lib = { version = "0.34.3", path = "crates/egui_demo_lib", default-features = false }
|
||||
egui_glow = { version = "0.34.3", path = "crates/egui_glow", default-features = false }
|
||||
egui_kittest = { version = "0.34.3", path = "crates/egui_kittest", default-features = false }
|
||||
eframe = { version = "0.34.3", path = "crates/eframe", default-features = false }
|
||||
|
||||
accesskit = "0.24.0"
|
||||
accesskit_consumer = "0.35.0"
|
||||
|
||||
@@ -6,6 +6,10 @@ This file is updated upon each release.
|
||||
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
|
||||
|
||||
|
||||
## 0.34.3 - 2026-05-27
|
||||
Nothing new
|
||||
|
||||
|
||||
## 0.34.2 - 2026-05-04
|
||||
Nothing new
|
||||
|
||||
|
||||
@@ -7,6 +7,11 @@ This file is updated upon each release.
|
||||
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
|
||||
|
||||
|
||||
## 0.34.3 - 2026-05-27
|
||||
* Default `app_id` to `app_name` on native [#8172](https://github.com/emilk/egui/pull/8172) by [@grtlr](https://github.com/grtlr)
|
||||
* Add winit window access to `eframe::Frame` and `CreationContext` [#8205](https://github.com/emilk/egui/pull/8205) by [@emilk](https://github.com/emilk)
|
||||
|
||||
|
||||
## 0.34.2 - 2026-05-04
|
||||
* Document glow-only fields in `NativeOptions` [#8104](https://github.com/emilk/egui/pull/8104) by [@emilk](https://github.com/emilk)
|
||||
|
||||
|
||||
@@ -6,6 +6,10 @@ This file is updated upon each release.
|
||||
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
|
||||
|
||||
|
||||
## 0.34.3 - 2026-05-27
|
||||
* Fix random hangs by improving `wgpu::Surface` lifecycle handling [#8171](https://github.com/emilk/egui/pull/8171) by [@grtlr](https://github.com/grtlr)
|
||||
|
||||
|
||||
## 0.34.2 - 2026-05-04
|
||||
* Update to wgpu 29.0.1 [#8073](https://github.com/emilk/egui/pull/8073) by [@emilk](https://github.com/emilk)
|
||||
* Warn if using a software rasterizer [#8101](https://github.com/emilk/egui/pull/8101) by [@emilk](https://github.com/emilk)
|
||||
|
||||
@@ -5,6 +5,10 @@ This file is updated upon each release.
|
||||
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
|
||||
|
||||
|
||||
## 0.34.3 - 2026-05-27
|
||||
Nothing new
|
||||
|
||||
|
||||
## 0.34.2 - 2026-05-04
|
||||
Nothing new
|
||||
|
||||
|
||||
@@ -1600,6 +1600,22 @@ fn key_from_key_code(key: winit::keyboard::KeyCode) -> Option<egui::Key> {
|
||||
KeyCode::F34 => Key::F34,
|
||||
KeyCode::F35 => Key::F35,
|
||||
|
||||
// Modifier keys — egui now surfaces them as distinct physical
|
||||
// variants so games / capture UIs can bind them independently.
|
||||
// The collapsed `Modifiers.shift/ctrl/alt/command` booleans still
|
||||
// track just the "any side is pressed" state for shortcut matching.
|
||||
KeyCode::ShiftLeft => Key::ShiftLeft,
|
||||
KeyCode::ShiftRight => Key::ShiftRight,
|
||||
KeyCode::ControlLeft => Key::ControlLeft,
|
||||
KeyCode::ControlRight => Key::ControlRight,
|
||||
KeyCode::AltLeft => Key::AltLeft,
|
||||
KeyCode::AltRight => Key::AltRight,
|
||||
KeyCode::SuperLeft => Key::SuperLeft,
|
||||
KeyCode::SuperRight => Key::SuperRight,
|
||||
|
||||
// ISO 102nd key — `<>|` on French AZERTY, `\|` on UK QWERTY.
|
||||
KeyCode::IntlBackslash => Key::IntlBackslash,
|
||||
|
||||
_ => {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -753,7 +753,12 @@ impl ScrollArea {
|
||||
ctx.animate_bool_responsive(id.with("v"), show_bars[1]),
|
||||
);
|
||||
|
||||
let current_bar_use = show_bars_factor.yx() * ui.spacing().scroll.allocated_width();
|
||||
let scroll_style = ui.spacing().scroll;
|
||||
let current_bar_use = if scroll_style.floating {
|
||||
show_bars.to_vec2().yx() * scroll_style.allocated_width()
|
||||
} else {
|
||||
show_bars_factor.yx() * scroll_style.allocated_width()
|
||||
};
|
||||
|
||||
let available_outer = ui.available_rect_before_wrap();
|
||||
|
||||
@@ -1042,17 +1047,13 @@ impl ScrollArea {
|
||||
.inner;
|
||||
|
||||
let (content_size, state) = prepared.end(ui);
|
||||
let output = ScrollAreaOutput {
|
||||
ScrollAreaOutput {
|
||||
inner,
|
||||
id,
|
||||
state,
|
||||
content_size,
|
||||
inner_rect,
|
||||
};
|
||||
|
||||
paint_fade_areas(ui, &output);
|
||||
|
||||
output
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1191,9 +1192,15 @@ impl Prepared {
|
||||
|
||||
let outer_rect = Rect::from_min_size(inner_rect.min, inner_rect.size() + current_bar_use);
|
||||
|
||||
let limit_rect = if ui.spacing().scroll.floating {
|
||||
outer_rect
|
||||
} else {
|
||||
inner_rect
|
||||
};
|
||||
|
||||
let content_is_too_large = Vec2b::new(
|
||||
direction_enabled[0] && inner_rect.width() < content_size.x,
|
||||
direction_enabled[1] && inner_rect.height() < content_size.y,
|
||||
direction_enabled[0] && (limit_rect.width().ceil() < content_size.x),
|
||||
direction_enabled[1] && (limit_rect.height().ceil() < content_size.y),
|
||||
);
|
||||
|
||||
let max_offset = content_size - inner_rect.size();
|
||||
@@ -1260,6 +1267,11 @@ impl Prepared {
|
||||
|
||||
let scroll_style = ui.spacing().scroll;
|
||||
|
||||
// Reserve the scroll area before painting fades, because fade painting uses ui.min_rect().
|
||||
ui.advance_cursor_after_rect(outer_rect);
|
||||
|
||||
paint_fade_areas_impl(ui, inner_rect, content_size, state.offset);
|
||||
|
||||
// Paint the bars:
|
||||
let scroll_bar_rect = scroll_bar_rect.unwrap_or(inner_rect);
|
||||
for d in 0..2 {
|
||||
@@ -1508,8 +1520,6 @@ impl Prepared {
|
||||
}
|
||||
}
|
||||
|
||||
ui.advance_cursor_after_rect(outer_rect);
|
||||
|
||||
if show_scroll_this_frame != state.show_scroll {
|
||||
ui.request_repaint();
|
||||
}
|
||||
@@ -1551,7 +1561,7 @@ impl Prepared {
|
||||
|
||||
/// Paint fade-out gradients at the top and/or bottom of a scroll area to
|
||||
/// indicate that more content is available beyond the visible region.
|
||||
fn paint_fade_areas<R>(ui: &Ui, scroll_output: &ScrollAreaOutput<R>) {
|
||||
fn paint_fade_areas_impl(ui: &Ui, inner_rect: Rect, content_size: Vec2, offset: Vec2) {
|
||||
let crate::style::ScrollFadeStyle {
|
||||
strength,
|
||||
size: fade_size,
|
||||
@@ -1563,11 +1573,9 @@ fn paint_fade_areas<R>(ui: &Ui, scroll_output: &ScrollAreaOutput<R>) {
|
||||
|
||||
let bg = ui.stack().bg_color();
|
||||
|
||||
let offset = scroll_output.state.offset;
|
||||
let overflow = scroll_output.content_size - scroll_output.inner_rect.size();
|
||||
let overflow = content_size - inner_rect.size();
|
||||
|
||||
let paint_rect = scroll_output
|
||||
.inner_rect
|
||||
let paint_rect = inner_rect
|
||||
.intersect(ui.min_rect())
|
||||
.expand(ui.visuals().clip_rect_margin);
|
||||
|
||||
|
||||
@@ -1307,6 +1307,52 @@ impl Context {
|
||||
.map(|widget_rect| self.get_response(widget_rect))
|
||||
}
|
||||
|
||||
/// Rectangles that could receive pointer input in the last completed pass.
|
||||
///
|
||||
/// This exposes the same widget rectangles egui uses for hit-testing, after
|
||||
/// filtering out disabled widgets, non-interactive widgets, and layers that
|
||||
/// are currently blocked from interaction. The returned rectangles are in
|
||||
/// global viewport coordinates, with layer transforms applied.
|
||||
///
|
||||
/// This is meant for integrations that must declare platform input regions
|
||||
/// before pointer events can be delivered to egui, such as transparent or
|
||||
/// click-through overlays.
|
||||
#[must_use]
|
||||
pub fn interactive_rects_last_pass(&self) -> Vec<Rect> {
|
||||
self.read(|ctx| {
|
||||
let Some(viewport) = ctx.viewports.get(&ctx.viewport_id()) else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let mut layers: Vec<LayerId> = viewport.prev_pass.widgets.layer_ids().collect();
|
||||
layers.sort_by(|&a, &b| ctx.memory.areas().compare_order(a, b));
|
||||
|
||||
let mut rects = Vec::new();
|
||||
for layer_id in layers {
|
||||
if !ctx.memory.allows_interaction(layer_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let to_global = ctx.memory.to_global.get(&layer_id).copied();
|
||||
for widget in viewport.prev_pass.widgets.get_layer(layer_id) {
|
||||
if !widget.enabled || !widget.sense.interactive() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let rect = if let Some(to_global) = to_global {
|
||||
to_global * widget.interact_rect
|
||||
} else {
|
||||
widget.interact_rect
|
||||
};
|
||||
if rect.is_positive() && rect.is_finite() {
|
||||
rects.push(rect);
|
||||
}
|
||||
}
|
||||
}
|
||||
rects
|
||||
})
|
||||
}
|
||||
|
||||
/// Do all interaction for an existing widget, without (re-)registering it.
|
||||
pub(crate) fn get_response(&self, widget_rect: WidgetRect) -> Response {
|
||||
use response::Flags;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
19
crates/egui/src/data/input/dropped_file.rs
Normal file
19
crates/egui/src/data/input/dropped_file.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
/// A file dropped into egui.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct DroppedFile {
|
||||
/// Set by the `egui-winit` backend.
|
||||
pub path: Option<std::path::PathBuf>,
|
||||
|
||||
/// Name of the file. Set by the `eframe` web backend.
|
||||
pub name: String,
|
||||
|
||||
/// With the `eframe` web backend, this is set to the mime-type of the file (if available).
|
||||
pub mime: String,
|
||||
|
||||
/// Set by the `eframe` web backend.
|
||||
pub last_modified: Option<std::time::SystemTime>,
|
||||
|
||||
/// Set by the `eframe` web backend.
|
||||
pub bytes: Option<std::sync::Arc<[u8]>>,
|
||||
}
|
||||
186
crates/egui/src/data/input/event.rs
Normal file
186
crates/egui/src/data/input/event.rs
Normal file
@@ -0,0 +1,186 @@
|
||||
use epaint::ColorImage;
|
||||
|
||||
use crate::{
|
||||
Key,
|
||||
emath::{Pos2, Vec2},
|
||||
};
|
||||
|
||||
use super::{
|
||||
ImeEvent, Modifiers, MouseWheelUnit, PointerButton, TouchDeviceId, TouchId, TouchPhase,
|
||||
};
|
||||
|
||||
/// An input event generated by the integration.
|
||||
///
|
||||
/// This only covers events that egui cares about.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub enum Event {
|
||||
/// The integration detected a "copy" event (e.g. Cmd+C).
|
||||
Copy,
|
||||
|
||||
/// The integration detected a "cut" event (e.g. Cmd+X).
|
||||
Cut,
|
||||
|
||||
/// The integration detected a "paste" event (e.g. Cmd+V).
|
||||
Paste(String),
|
||||
|
||||
/// Text input, e.g. via keyboard.
|
||||
///
|
||||
/// When the user presses enter/return, do not send a [`Text`](Event::Text) (just [`Key::Enter`]).
|
||||
Text(String),
|
||||
|
||||
/// A key was pressed or released.
|
||||
///
|
||||
/// ## Note for integration authors
|
||||
///
|
||||
/// Key events that has been processed by IMEs should not be sent to `egui`.
|
||||
Key {
|
||||
/// Most of the time, it's the logical key, heeding the active keymap -- for instance, if the user has Dvorak
|
||||
/// keyboard layout, it will be taken into account.
|
||||
///
|
||||
/// If it's impossible to determine the logical key on desktop platforms (say, in case of non-Latin letters),
|
||||
/// `key` falls back to the value of the corresponding physical key. This is necessary for proper work of
|
||||
/// standard shortcuts that only respond to Latin-based bindings (such as `Ctrl` + `V`).
|
||||
key: Key,
|
||||
|
||||
/// The physical key, corresponding to the actual position on the keyboard.
|
||||
///
|
||||
/// This ignores keymaps, so it is not recommended to use this.
|
||||
/// The only thing it makes sense for is things like games,
|
||||
/// where e.g. the physical location of WSAD on QWERTY should always map to movement,
|
||||
/// even if the user is using Dvorak or AZERTY.
|
||||
///
|
||||
/// `eframe` does not (yet) implement this on web.
|
||||
physical_key: Option<Key>,
|
||||
|
||||
/// Was it pressed or released?
|
||||
pressed: bool,
|
||||
|
||||
/// If this is a `pressed` event, is it a key-repeat?
|
||||
///
|
||||
/// On many platforms, holding down a key produces many repeated "pressed" events for it, so called key-repeats.
|
||||
/// Sometimes you will want to ignore such events, and this lets you do that.
|
||||
///
|
||||
/// egui will automatically detect such repeat events and mark them as such here.
|
||||
/// Therefore, if you are writing an egui integration, you do not need to set this (just set it to `false`).
|
||||
repeat: bool,
|
||||
|
||||
/// The state of the modifier keys at the time of the event.
|
||||
modifiers: Modifiers,
|
||||
},
|
||||
|
||||
/// The mouse or touch moved to a new place.
|
||||
PointerMoved(Pos2),
|
||||
|
||||
/// The mouse moved, the units are unspecified.
|
||||
/// Represents the actual movement of the mouse, without acceleration or clamped by screen edges.
|
||||
/// `PointerMoved` and `MouseMoved` can be sent at the same time.
|
||||
/// This event is optional. If the integration can not determine unfiltered motion it should not send this event.
|
||||
MouseMoved(Vec2),
|
||||
|
||||
/// A mouse button was pressed or released (or a touch started or stopped).
|
||||
PointerButton {
|
||||
/// Where is the pointer?
|
||||
pos: Pos2,
|
||||
|
||||
/// What mouse button? For touches, use [`PointerButton::Primary`].
|
||||
button: PointerButton,
|
||||
|
||||
/// Was it the button/touch pressed this frame, or released?
|
||||
pressed: bool,
|
||||
|
||||
/// The state of the modifier keys at the time of the event.
|
||||
modifiers: Modifiers,
|
||||
},
|
||||
|
||||
/// The mouse left the screen, or the last/primary touch input disappeared.
|
||||
///
|
||||
/// This means there is no longer a cursor on the screen for hovering etc.
|
||||
///
|
||||
/// On touch-up first send `PointerButton{pressed: false, …}` followed by `PointerLeft`.
|
||||
PointerGone,
|
||||
|
||||
/// Zoom scale factor this frame (e.g. from a pinch gesture).
|
||||
///
|
||||
/// * `zoom = 1`: no change.
|
||||
/// * `zoom < 1`: pinch together
|
||||
/// * `zoom > 1`: pinch spread
|
||||
///
|
||||
/// Note that egui also implement zooming by holding `Ctrl` and scrolling the mouse wheel,
|
||||
/// so integration need NOT emit this `Zoom` event in those cases, just [`Self::MouseWheel`].
|
||||
///
|
||||
/// As a user, check [`crate::InputState::smooth_scroll_delta`] to see if the user did any zooming this frame.
|
||||
Zoom(f32),
|
||||
|
||||
/// Rotation in radians this frame, measuring clockwise (e.g. from a rotation gesture).
|
||||
Rotate(f32),
|
||||
|
||||
/// IME Event
|
||||
Ime(ImeEvent),
|
||||
|
||||
/// On touch screens, report this *in addition to*
|
||||
/// [`Self::PointerMoved`], [`Self::PointerButton`], [`Self::PointerGone`]
|
||||
Touch {
|
||||
/// Hashed device identifier (if available; may be zero).
|
||||
/// Can be used to separate touches from different devices.
|
||||
device_id: TouchDeviceId,
|
||||
|
||||
/// Unique identifier of a finger/pen. Value is stable from touch down
|
||||
/// to lift-up
|
||||
id: TouchId,
|
||||
|
||||
/// One of: start move end cancel.
|
||||
phase: TouchPhase,
|
||||
|
||||
/// Position of the touch (or where the touch was last detected)
|
||||
pos: Pos2,
|
||||
|
||||
/// Describes how hard the touch device was pressed. May always be `None` if the platform does
|
||||
/// not support pressure sensitivity.
|
||||
/// The value is in the range from 0.0 (no pressure) to 1.0 (maximum pressure).
|
||||
force: Option<f32>,
|
||||
},
|
||||
|
||||
/// A raw mouse wheel event as sent by the backend.
|
||||
///
|
||||
/// Used for scrolling.
|
||||
MouseWheel {
|
||||
/// The unit of `delta`: points, lines, or pages.
|
||||
unit: MouseWheelUnit,
|
||||
|
||||
/// The direction of the vector indicates how to move the _content_ that is being viewed.
|
||||
/// So if you get positive values, the content being viewed should move to the right and down,
|
||||
/// revealing new things to the left and up.
|
||||
///
|
||||
/// A positive X-value indicates the content is being moved right,
|
||||
/// as when swiping right on a touch-screen or track-pad with natural scrolling.
|
||||
///
|
||||
/// A positive Y-value indicates the content is being moved down,
|
||||
/// as when swiping down on a touch-screen or track-pad with natural scrolling.
|
||||
delta: Vec2,
|
||||
|
||||
/// The phase of the scroll, useful for trackpads.
|
||||
///
|
||||
/// If unknown set this to [`TouchPhase::Move`].
|
||||
phase: TouchPhase,
|
||||
|
||||
/// The state of the modifier keys at the time of the event.
|
||||
modifiers: Modifiers,
|
||||
},
|
||||
|
||||
/// The native window gained or lost focused (e.g. the user clicked alt-tab).
|
||||
WindowFocused(bool),
|
||||
|
||||
/// An assistive technology (e.g. screen reader) requested an action.
|
||||
AccessKitActionRequest(accesskit::ActionRequest),
|
||||
|
||||
/// The reply of a screenshot requested with [`crate::ViewportCommand::Screenshot`].
|
||||
Screenshot {
|
||||
viewport_id: crate::ViewportId,
|
||||
|
||||
/// Whatever was passed to [`crate::ViewportCommand::Screenshot`].
|
||||
user_data: crate::UserData,
|
||||
|
||||
image: std::sync::Arc<ColorImage>,
|
||||
},
|
||||
}
|
||||
63
crates/egui/src/data/input/event_filter.rs
Normal file
63
crates/egui/src/data/input/event_filter.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
use super::Event;
|
||||
|
||||
// TODO(emilk): generalize this to a proper event filter.
|
||||
/// Controls which events that a focused widget will have exclusive access to.
|
||||
///
|
||||
/// Currently this only controls a few special keyboard events,
|
||||
/// but in the future this `struct` should be extended into a full callback thing.
|
||||
///
|
||||
/// Any events not covered by the filter are given to the widget, but are not exclusive.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct EventFilter {
|
||||
/// If `true`, pressing tab will act on the widget,
|
||||
/// and NOT move focus away from the focused widget.
|
||||
///
|
||||
/// Default: `false`
|
||||
pub tab: bool,
|
||||
|
||||
/// If `true`, pressing horizontal arrows will act on the
|
||||
/// widget, and NOT move focus away from the focused widget.
|
||||
///
|
||||
/// Default: `false`
|
||||
pub horizontal_arrows: bool,
|
||||
|
||||
/// If `true`, pressing vertical arrows will act on the
|
||||
/// widget, and NOT move focus away from the focused widget.
|
||||
///
|
||||
/// Default: `false`
|
||||
pub vertical_arrows: bool,
|
||||
|
||||
/// If `true`, pressing escape will act on the widget,
|
||||
/// and NOT surrender focus from the focused widget.
|
||||
///
|
||||
/// Default: `false`
|
||||
pub escape: bool,
|
||||
}
|
||||
|
||||
#[expect(clippy::derivable_impls)] // let's be explicit
|
||||
impl Default for EventFilter {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
tab: false,
|
||||
horizontal_arrows: false,
|
||||
vertical_arrows: false,
|
||||
escape: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventFilter {
|
||||
pub fn matches(&self, event: &Event) -> bool {
|
||||
if let Event::Key { key, .. } = event {
|
||||
match key {
|
||||
crate::Key::Tab => self.tab,
|
||||
crate::Key::ArrowUp | crate::Key::ArrowDown => self.vertical_arrows,
|
||||
crate::Key::ArrowRight | crate::Key::ArrowLeft => self.horizontal_arrows,
|
||||
crate::Key::Escape => self.escape,
|
||||
_ => true,
|
||||
}
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
10
crates/egui/src/data/input/hovered_file.rs
Normal file
10
crates/egui/src/data/input/hovered_file.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
/// A file about to be dropped into egui.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct HoveredFile {
|
||||
/// Set by the `egui-winit` backend.
|
||||
pub path: Option<std::path::PathBuf>,
|
||||
|
||||
/// With the `eframe` web backend, this is set to the mime-type of the file (if available).
|
||||
pub mime: String,
|
||||
}
|
||||
28
crates/egui/src/data/input/ime_event.rs
Normal file
28
crates/egui/src/data/input/ime_event.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
/// IME event.
|
||||
///
|
||||
/// See <https://docs.rs/winit/latest/winit/event/enum.Ime.html>
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub enum ImeEvent {
|
||||
/// Notifies when the IME was enabled.
|
||||
#[deprecated = "No longer used by egui"]
|
||||
Enabled,
|
||||
|
||||
/// A new IME candidate is being suggested.
|
||||
///
|
||||
/// An empty preedit string indicates that the IME has been dismissed, while
|
||||
/// a non-empty preedit string indicates that the IME is active.
|
||||
Preedit {
|
||||
text: String,
|
||||
active_range_chars: Option<std::ops::Range<usize>>,
|
||||
},
|
||||
|
||||
/// IME composition ended with this final result.
|
||||
///
|
||||
/// The IME is considered dismissed after this event.
|
||||
Commit(String),
|
||||
|
||||
/// Notifies when the IME was disabled.
|
||||
#[deprecated = "No longer used by egui"]
|
||||
Disabled,
|
||||
}
|
||||
52
crates/egui/src/data/input/keyboard_shortcut.rs
Normal file
52
crates/egui/src/data/input/keyboard_shortcut.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
use crate::Key;
|
||||
|
||||
use super::{ModifierNames, Modifiers};
|
||||
|
||||
/// A keyboard shortcut, e.g. `Ctrl+Alt+W`.
|
||||
///
|
||||
/// Can be used with [`crate::InputState::consume_shortcut`]
|
||||
/// and [`crate::Context::format_shortcut`].
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct KeyboardShortcut {
|
||||
pub modifiers: Modifiers,
|
||||
|
||||
pub logical_key: Key,
|
||||
}
|
||||
|
||||
impl KeyboardShortcut {
|
||||
pub const fn new(modifiers: Modifiers, logical_key: Key) -> Self {
|
||||
Self {
|
||||
modifiers,
|
||||
logical_key,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format(&self, names: &ModifierNames<'_>, is_mac: bool) -> String {
|
||||
let mut s = names.format(&self.modifiers, is_mac);
|
||||
if !s.is_empty() {
|
||||
s += names.concat;
|
||||
}
|
||||
if names.is_short {
|
||||
s += self.logical_key.symbol_or_name();
|
||||
} else {
|
||||
s += self.logical_key.name();
|
||||
}
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_kb_shortcut() {
|
||||
let cmd_shift_f = KeyboardShortcut::new(Modifiers::COMMAND | Modifiers::SHIFT, Key::F);
|
||||
assert_eq!(
|
||||
cmd_shift_f.format(&ModifierNames::NAMES, false),
|
||||
"Ctrl+Shift+F"
|
||||
);
|
||||
assert_eq!(
|
||||
cmd_shift_f.format(&ModifierNames::NAMES, true),
|
||||
"Shift+Cmd+F"
|
||||
);
|
||||
assert_eq!(cmd_shift_f.format(&ModifierNames::SYMBOLS, false), "⌃⇧F");
|
||||
assert_eq!(cmd_shift_f.format(&ModifierNames::SYMBOLS, true), "⇧⌘F");
|
||||
}
|
||||
33
crates/egui/src/data/input/mod.rs
Normal file
33
crates/egui/src/data/input/mod.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
//! The input needed by egui.
|
||||
|
||||
mod dropped_file;
|
||||
mod event;
|
||||
mod event_filter;
|
||||
mod hovered_file;
|
||||
mod ime_event;
|
||||
mod keyboard_shortcut;
|
||||
mod modifier_names;
|
||||
mod modifiers;
|
||||
mod mouse_wheel_unit;
|
||||
mod pointer_button;
|
||||
mod raw_input;
|
||||
mod safe_area_insets;
|
||||
mod touch;
|
||||
mod viewport_info;
|
||||
|
||||
pub use self::{
|
||||
dropped_file::DroppedFile,
|
||||
event::Event,
|
||||
event_filter::EventFilter,
|
||||
hovered_file::HoveredFile,
|
||||
ime_event::ImeEvent,
|
||||
keyboard_shortcut::KeyboardShortcut,
|
||||
modifier_names::ModifierNames,
|
||||
modifiers::Modifiers,
|
||||
mouse_wheel_unit::MouseWheelUnit,
|
||||
pointer_button::{NUM_POINTER_BUTTONS, PointerButton},
|
||||
raw_input::RawInput,
|
||||
safe_area_insets::SafeAreaInsets,
|
||||
touch::{TouchDeviceId, TouchId, TouchPhase},
|
||||
viewport_info::{ViewportEvent, ViewportInfo},
|
||||
};
|
||||
70
crates/egui/src/data/input/modifier_names.rs
Normal file
70
crates/egui/src/data/input/modifier_names.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
use super::Modifiers;
|
||||
|
||||
/// Names of different modifier keys.
|
||||
///
|
||||
/// Used to name modifiers.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct ModifierNames<'a> {
|
||||
pub is_short: bool,
|
||||
|
||||
pub alt: &'a str,
|
||||
pub ctrl: &'a str,
|
||||
pub shift: &'a str,
|
||||
pub mac_cmd: &'a str,
|
||||
pub mac_alt: &'a str,
|
||||
|
||||
/// What goes between the names
|
||||
pub concat: &'a str,
|
||||
}
|
||||
|
||||
impl ModifierNames<'static> {
|
||||
/// ⌥ ⌃ ⇧ ⌘ - NOTE: not supported by the default egui font.
|
||||
pub const SYMBOLS: Self = Self {
|
||||
is_short: true,
|
||||
alt: "⌥",
|
||||
ctrl: "⌃",
|
||||
shift: "⇧",
|
||||
mac_cmd: "⌘",
|
||||
mac_alt: "⌥",
|
||||
concat: "",
|
||||
};
|
||||
|
||||
/// Alt, Ctrl, Shift, Cmd
|
||||
pub const NAMES: Self = Self {
|
||||
is_short: false,
|
||||
alt: "Alt",
|
||||
ctrl: "Ctrl",
|
||||
shift: "Shift",
|
||||
mac_cmd: "Cmd",
|
||||
mac_alt: "Option",
|
||||
concat: "+",
|
||||
};
|
||||
}
|
||||
|
||||
impl ModifierNames<'_> {
|
||||
pub fn format(&self, modifiers: &Modifiers, is_mac: bool) -> String {
|
||||
let mut s = String::new();
|
||||
|
||||
let mut append_if = |modifier_is_active, modifier_name| {
|
||||
if modifier_is_active {
|
||||
if !s.is_empty() {
|
||||
s += self.concat;
|
||||
}
|
||||
s += modifier_name;
|
||||
}
|
||||
};
|
||||
|
||||
if is_mac {
|
||||
append_if(modifiers.ctrl, self.ctrl);
|
||||
append_if(modifiers.shift, self.shift);
|
||||
append_if(modifiers.alt, self.mac_alt);
|
||||
append_if(modifiers.mac_cmd || modifiers.command, self.mac_cmd);
|
||||
} else {
|
||||
append_if(modifiers.ctrl || modifiers.command, self.ctrl);
|
||||
append_if(modifiers.alt, self.alt);
|
||||
append_if(modifiers.shift, self.shift);
|
||||
}
|
||||
|
||||
s
|
||||
}
|
||||
}
|
||||
410
crates/egui/src/data/input/modifiers.rs
Normal file
410
crates/egui/src/data/input/modifiers.rs
Normal file
@@ -0,0 +1,410 @@
|
||||
use super::ModifierNames;
|
||||
|
||||
/// State of the modifier keys. These must be fed to egui.
|
||||
///
|
||||
/// 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.
|
||||
#[derive(Clone, Copy, Default, Hash, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct Modifiers {
|
||||
/// Either of the alt keys are down (option ⌥ on Mac).
|
||||
pub alt: bool,
|
||||
|
||||
/// Either of the control keys are down.
|
||||
/// When checking for keyboard shortcuts, consider using [`Self::command`] instead.
|
||||
pub ctrl: bool,
|
||||
|
||||
/// Either of the shift keys are down.
|
||||
pub shift: bool,
|
||||
|
||||
/// The Mac ⌘ Command key. Should always be set to `false` on other platforms.
|
||||
pub mac_cmd: bool,
|
||||
|
||||
/// On Windows and Linux, set this to the same value as `ctrl`.
|
||||
/// On Mac, this should be set whenever one of the ⌘ Command keys are down (same as `mac_cmd`).
|
||||
/// This is so that egui can, for instance, select all text by checking for `command + A`
|
||||
/// and it will work on both Mac and Windows.
|
||||
pub command: bool,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Modifiers {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if self.is_none() {
|
||||
return write!(f, "Modifiers::NONE");
|
||||
}
|
||||
|
||||
let Self {
|
||||
alt,
|
||||
ctrl,
|
||||
shift,
|
||||
mac_cmd,
|
||||
command,
|
||||
} = *self;
|
||||
|
||||
let mut debug = f.debug_struct("Modifiers");
|
||||
if alt {
|
||||
debug.field("alt", &true);
|
||||
}
|
||||
if ctrl {
|
||||
debug.field("ctrl", &true);
|
||||
}
|
||||
if shift {
|
||||
debug.field("shift", &true);
|
||||
}
|
||||
if mac_cmd {
|
||||
debug.field("mac_cmd", &true);
|
||||
}
|
||||
if command {
|
||||
debug.field("command", &true);
|
||||
}
|
||||
debug.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Modifiers {
|
||||
pub const NONE: Self = Self {
|
||||
alt: false,
|
||||
ctrl: false,
|
||||
shift: false,
|
||||
mac_cmd: false,
|
||||
command: false,
|
||||
};
|
||||
|
||||
pub const ALT: Self = Self {
|
||||
alt: true,
|
||||
ctrl: false,
|
||||
shift: false,
|
||||
mac_cmd: false,
|
||||
command: false,
|
||||
};
|
||||
pub const CTRL: Self = Self {
|
||||
alt: false,
|
||||
ctrl: true,
|
||||
shift: false,
|
||||
mac_cmd: false,
|
||||
command: false,
|
||||
};
|
||||
pub const SHIFT: Self = Self {
|
||||
alt: false,
|
||||
ctrl: false,
|
||||
shift: true,
|
||||
mac_cmd: false,
|
||||
command: false,
|
||||
};
|
||||
|
||||
/// The Mac ⌘ Command key
|
||||
pub const MAC_CMD: Self = Self {
|
||||
alt: false,
|
||||
ctrl: false,
|
||||
shift: false,
|
||||
mac_cmd: true,
|
||||
command: false,
|
||||
};
|
||||
|
||||
/// On Mac: ⌘ Command key, elsewhere: Ctrl key
|
||||
pub const COMMAND: Self = Self {
|
||||
alt: false,
|
||||
ctrl: false,
|
||||
shift: false,
|
||||
mac_cmd: false,
|
||||
command: true,
|
||||
};
|
||||
|
||||
/// ```
|
||||
/// # use egui::Modifiers;
|
||||
/// assert_eq!(
|
||||
/// Modifiers::CTRL | Modifiers::ALT,
|
||||
/// Modifiers { ctrl: true, alt: true, ..Default::default() }
|
||||
/// );
|
||||
/// assert_eq!(
|
||||
/// Modifiers::ALT.plus(Modifiers::CTRL),
|
||||
/// Modifiers::CTRL.plus(Modifiers::ALT),
|
||||
/// );
|
||||
/// assert_eq!(
|
||||
/// Modifiers::CTRL | Modifiers::ALT,
|
||||
/// Modifiers::CTRL.plus(Modifiers::ALT),
|
||||
/// );
|
||||
/// ```
|
||||
#[inline]
|
||||
pub const fn plus(self, rhs: Self) -> Self {
|
||||
Self {
|
||||
alt: self.alt | rhs.alt,
|
||||
ctrl: self.ctrl | rhs.ctrl,
|
||||
shift: self.shift | rhs.shift,
|
||||
mac_cmd: self.mac_cmd | rhs.mac_cmd,
|
||||
command: self.command | rhs.command,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_none(&self) -> bool {
|
||||
self == &Self::default()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn any(&self) -> bool {
|
||||
!self.is_none()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn all(&self) -> bool {
|
||||
self.alt && self.ctrl && self.shift && self.command
|
||||
}
|
||||
|
||||
/// Is shift the only pressed button?
|
||||
#[inline]
|
||||
pub fn shift_only(&self) -> bool {
|
||||
self.shift && !(self.alt || self.command)
|
||||
}
|
||||
|
||||
/// true if only [`Self::ctrl`] or only [`Self::mac_cmd`] is pressed.
|
||||
#[inline]
|
||||
pub fn command_only(&self) -> bool {
|
||||
!self.alt && !self.shift && self.command
|
||||
}
|
||||
|
||||
/// Checks that the `ctrl/cmd` matches, and that the `shift/alt` of the argument is a subset
|
||||
/// of the pressed key (`self`).
|
||||
///
|
||||
/// This means that if the pattern has not set `shift`, then `self` can have `shift` set or not.
|
||||
///
|
||||
/// The reason is that many logical keys require `shift` or `alt` on some keyboard layouts.
|
||||
/// For instance, in order to press `+` on an English keyboard, you need to press `shift` and `=`,
|
||||
/// but a Swedish keyboard has dedicated `+` key.
|
||||
/// So if you want to make a [`KeyboardShortcut`](crate::KeyboardShortcut) looking for `Cmd` + `+`, it makes sense
|
||||
/// to ignore the shift key.
|
||||
/// Similarly, the `Alt` key is sometimes used to type special characters.
|
||||
///
|
||||
/// However, if the pattern (the argument) explicitly requires the `shift` or `alt` keys
|
||||
/// to be pressed, then they must be pressed.
|
||||
///
|
||||
/// # Example:
|
||||
/// ```
|
||||
/// # use egui::Modifiers;
|
||||
/// # let pressed_modifiers = Modifiers::default();
|
||||
/// if pressed_modifiers.matches_logically(Modifiers::ALT | Modifiers::SHIFT) {
|
||||
/// // Alt and Shift are pressed, but not ctrl/command
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## Behavior:
|
||||
/// ```
|
||||
/// # use egui::Modifiers;
|
||||
/// assert!(Modifiers::CTRL.matches_logically(Modifiers::CTRL));
|
||||
/// assert!(!Modifiers::CTRL.matches_logically(Modifiers::CTRL | Modifiers::SHIFT));
|
||||
/// assert!((Modifiers::CTRL | Modifiers::SHIFT).matches_logically(Modifiers::CTRL));
|
||||
/// assert!((Modifiers::CTRL | Modifiers::COMMAND).matches_logically(Modifiers::CTRL));
|
||||
/// assert!((Modifiers::CTRL | Modifiers::COMMAND).matches_logically(Modifiers::COMMAND));
|
||||
/// assert!((Modifiers::MAC_CMD | Modifiers::COMMAND).matches_logically(Modifiers::COMMAND));
|
||||
/// assert!(!Modifiers::COMMAND.matches_logically(Modifiers::MAC_CMD));
|
||||
/// ```
|
||||
pub fn matches_logically(&self, pattern: Self) -> bool {
|
||||
if pattern.alt && !self.alt {
|
||||
return false;
|
||||
}
|
||||
if pattern.shift && !self.shift {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.cmd_ctrl_matches(pattern)
|
||||
}
|
||||
|
||||
/// Check for equality but with proper handling of [`Self::command`].
|
||||
///
|
||||
/// `self` here are the currently pressed modifiers,
|
||||
/// and the argument the pattern we are testing for.
|
||||
///
|
||||
/// Note that this will require the `shift` and `alt` keys match, even though
|
||||
/// these modifiers are sometimes required to produce some logical keys.
|
||||
/// For instance, to press `+` on an English keyboard, you need to press `shift` and `=`,
|
||||
/// but on a Swedish keyboard you can press the dedicated `+` key.
|
||||
/// Therefore, you often want to use [`Self::matches_logically`] instead.
|
||||
///
|
||||
/// # Example:
|
||||
/// ```
|
||||
/// # use egui::Modifiers;
|
||||
/// # let pressed_modifiers = Modifiers::default();
|
||||
/// if pressed_modifiers.matches_exact(Modifiers::ALT | Modifiers::SHIFT) {
|
||||
/// // Alt and Shift are pressed, and nothing else
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## Behavior:
|
||||
/// ```
|
||||
/// # use egui::Modifiers;
|
||||
/// assert!(Modifiers::CTRL.matches_exact(Modifiers::CTRL));
|
||||
/// assert!(!Modifiers::CTRL.matches_exact(Modifiers::CTRL | Modifiers::SHIFT));
|
||||
/// assert!(!(Modifiers::CTRL | Modifiers::SHIFT).matches_exact(Modifiers::CTRL));
|
||||
/// assert!((Modifiers::CTRL | Modifiers::COMMAND).matches_exact(Modifiers::CTRL));
|
||||
/// assert!((Modifiers::CTRL | Modifiers::COMMAND).matches_exact(Modifiers::COMMAND));
|
||||
/// assert!((Modifiers::MAC_CMD | Modifiers::COMMAND).matches_exact(Modifiers::COMMAND));
|
||||
/// assert!(!Modifiers::COMMAND.matches_exact(Modifiers::MAC_CMD));
|
||||
/// ```
|
||||
pub fn matches_exact(&self, pattern: Self) -> bool {
|
||||
// alt and shift must always match the pattern:
|
||||
if pattern.alt != self.alt || pattern.shift != self.shift {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.cmd_ctrl_matches(pattern)
|
||||
}
|
||||
|
||||
/// Check if any of the modifiers match exactly.
|
||||
///
|
||||
/// Returns true if the same modifier is pressed in `self` as in `pattern`,
|
||||
/// for at least one modifier.
|
||||
///
|
||||
/// ## Behavior:
|
||||
/// ```
|
||||
/// # use egui::Modifiers;
|
||||
/// assert!(Modifiers::CTRL.matches_any(Modifiers::CTRL));
|
||||
/// assert!(Modifiers::CTRL.matches_any(Modifiers::CTRL | Modifiers::SHIFT));
|
||||
/// assert!((Modifiers::CTRL | Modifiers::SHIFT).matches_any(Modifiers::CTRL));
|
||||
/// ```
|
||||
pub fn matches_any(&self, pattern: Self) -> bool {
|
||||
if self.alt && pattern.alt {
|
||||
return true;
|
||||
}
|
||||
if self.shift && pattern.shift {
|
||||
return true;
|
||||
}
|
||||
if self.ctrl && pattern.ctrl {
|
||||
return true;
|
||||
}
|
||||
if self.mac_cmd && pattern.mac_cmd {
|
||||
return true;
|
||||
}
|
||||
if (self.mac_cmd || self.command || self.ctrl) && pattern.command {
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Checks only cmd/ctrl, not alt/shift.
|
||||
///
|
||||
/// `self` here are the currently pressed modifiers,
|
||||
/// and the argument the pattern we are testing for.
|
||||
///
|
||||
/// This takes care to properly handle the difference between
|
||||
/// [`Self::ctrl`], [`Self::command`] and [`Self::mac_cmd`].
|
||||
pub fn cmd_ctrl_matches(&self, pattern: Self) -> bool {
|
||||
if pattern.mac_cmd {
|
||||
// Mac-specific match:
|
||||
if !self.mac_cmd {
|
||||
return false;
|
||||
}
|
||||
if pattern.ctrl != self.ctrl {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if !pattern.ctrl && !pattern.command {
|
||||
// the pattern explicitly doesn't want any ctrl/command:
|
||||
return !self.ctrl && !self.command;
|
||||
}
|
||||
|
||||
// if the pattern is looking for command, then `ctrl` may or may not be set depending on platform.
|
||||
// if the pattern is looking for `ctrl`, then `command` may or may not be set depending on platform.
|
||||
|
||||
if pattern.ctrl && !self.ctrl {
|
||||
return false;
|
||||
}
|
||||
if pattern.command && !self.command {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Whether another set of modifiers is contained in this set of modifiers with proper handling of [`Self::command`].
|
||||
///
|
||||
/// ```
|
||||
/// # use egui::Modifiers;
|
||||
/// assert!(Modifiers::default().contains(Modifiers::default()));
|
||||
/// assert!(Modifiers::CTRL.contains(Modifiers::default()));
|
||||
/// assert!(Modifiers::CTRL.contains(Modifiers::CTRL));
|
||||
/// assert!(Modifiers::CTRL.contains(Modifiers::COMMAND));
|
||||
/// assert!(Modifiers::MAC_CMD.contains(Modifiers::COMMAND));
|
||||
/// assert!(Modifiers::COMMAND.contains(Modifiers::MAC_CMD));
|
||||
/// assert!(Modifiers::COMMAND.contains(Modifiers::CTRL));
|
||||
/// assert!(!(Modifiers::ALT | Modifiers::CTRL).contains(Modifiers::SHIFT));
|
||||
/// assert!((Modifiers::CTRL | Modifiers::SHIFT).contains(Modifiers::CTRL));
|
||||
/// assert!(!Modifiers::CTRL.contains(Modifiers::CTRL | Modifiers::SHIFT));
|
||||
/// ```
|
||||
pub fn contains(&self, query: Self) -> bool {
|
||||
if query == Self::default() {
|
||||
return true;
|
||||
}
|
||||
|
||||
let Self {
|
||||
alt,
|
||||
ctrl,
|
||||
shift,
|
||||
mac_cmd,
|
||||
command,
|
||||
} = *self;
|
||||
|
||||
if alt && query.alt {
|
||||
return self.contains(Self {
|
||||
alt: false,
|
||||
..query
|
||||
});
|
||||
}
|
||||
if shift && query.shift {
|
||||
return self.contains(Self {
|
||||
shift: false,
|
||||
..query
|
||||
});
|
||||
}
|
||||
|
||||
if (ctrl || command) && (query.ctrl || query.command) {
|
||||
return self.contains(Self {
|
||||
command: false,
|
||||
ctrl: false,
|
||||
..query
|
||||
});
|
||||
}
|
||||
if (mac_cmd || command) && (query.mac_cmd || query.command) {
|
||||
return self.contains(Self {
|
||||
mac_cmd: false,
|
||||
command: false,
|
||||
..query
|
||||
});
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::BitOr for Modifiers {
|
||||
type Output = Self;
|
||||
|
||||
#[inline]
|
||||
fn bitor(self, rhs: Self) -> Self {
|
||||
self.plus(rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::BitOrAssign for Modifiers {
|
||||
#[inline]
|
||||
fn bitor_assign(&mut self, rhs: Self) {
|
||||
*self = *self | rhs;
|
||||
}
|
||||
}
|
||||
|
||||
impl Modifiers {
|
||||
pub fn ui(&self, ui: &mut crate::Ui) {
|
||||
ui.label(ModifierNames::NAMES.format(self, ui.ctx().os().is_mac()));
|
||||
}
|
||||
}
|
||||
13
crates/egui/src/data/input/mouse_wheel_unit.rs
Normal file
13
crates/egui/src/data/input/mouse_wheel_unit.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
/// The unit associated with the numeric value of a mouse wheel event
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub enum MouseWheelUnit {
|
||||
/// Number of ui points (logical pixels)
|
||||
Point,
|
||||
|
||||
/// Number of lines
|
||||
Line,
|
||||
|
||||
/// Number of pages
|
||||
Page,
|
||||
}
|
||||
23
crates/egui/src/data/input/pointer_button.rs
Normal file
23
crates/egui/src/data/input/pointer_button.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
/// Mouse button (or similar for touch input)
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub enum PointerButton {
|
||||
/// The primary mouse button is usually the left one.
|
||||
Primary = 0,
|
||||
|
||||
/// The secondary mouse button is usually the right one,
|
||||
/// and most often used for context menus or other optional things.
|
||||
Secondary = 1,
|
||||
|
||||
/// The tertiary mouse button is usually the middle mouse button (e.g. clicking the scroll wheel).
|
||||
Middle = 2,
|
||||
|
||||
/// The first extra mouse button on some mice. In web typically corresponds to the Browser back button.
|
||||
Extra1 = 3,
|
||||
|
||||
/// The second extra mouse button on some mice. In web typically corresponds to the Browser forward button.
|
||||
Extra2 = 4,
|
||||
}
|
||||
|
||||
/// Number of pointer buttons supported by egui, i.e. the number of possible states of [`PointerButton`].
|
||||
pub const NUM_POINTER_BUTTONS: usize = 5;
|
||||
225
crates/egui/src/data/input/raw_input.rs
Normal file
225
crates/egui/src/data/input/raw_input.rs
Normal file
@@ -0,0 +1,225 @@
|
||||
use crate::{OrderedViewportIdMap, Theme, ViewportId, ViewportIdMap, emath::Rect};
|
||||
|
||||
use super::{DroppedFile, Event, HoveredFile, Modifiers, SafeAreaInsets, ViewportInfo};
|
||||
|
||||
/// What the integrations provides to egui at the start of each frame.
|
||||
///
|
||||
/// Set the values that make sense, leave the rest at their `Default::default()`.
|
||||
///
|
||||
/// You can check if `egui` is using the inputs using
|
||||
/// [`crate::Context::egui_wants_pointer_input`] and [`crate::Context::egui_wants_keyboard_input`].
|
||||
///
|
||||
/// All coordinates are in points (logical pixels) with origin (0, 0) in the top left .corner.
|
||||
///
|
||||
/// Ii "points" can be calculated from native physical pixels
|
||||
/// using `pixels_per_point` = [`crate::Context::zoom_factor`] * `native_pixels_per_point`;
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct RawInput {
|
||||
/// The id of the active viewport.
|
||||
pub viewport_id: ViewportId,
|
||||
|
||||
/// Information about all egui viewports.
|
||||
pub viewports: ViewportIdMap<ViewportInfo>,
|
||||
|
||||
/// The insets used to only render content in a mobile safe area
|
||||
///
|
||||
/// `None` will be treated as "same as last frame"
|
||||
pub safe_area_insets: Option<SafeAreaInsets>,
|
||||
|
||||
/// Position and size of the area that egui should use, in points.
|
||||
/// Usually you would set this to
|
||||
///
|
||||
/// `Some(Rect::from_min_size(Default::default(), screen_size_in_points))`.
|
||||
///
|
||||
/// but you could also constrain egui to some smaller portion of your window if you like.
|
||||
///
|
||||
/// `None` will be treated as "same as last frame", with the default being a very big area.
|
||||
pub screen_rect: Option<Rect>,
|
||||
|
||||
/// Maximum size of one side of the font texture.
|
||||
///
|
||||
/// Ask your graphics drivers about this. This corresponds to `GL_MAX_TEXTURE_SIZE`.
|
||||
///
|
||||
/// The default is a very small (but very portable) 2048.
|
||||
pub max_texture_side: Option<usize>,
|
||||
|
||||
/// Monotonically increasing time, in seconds. Relative to whatever. Used for animations.
|
||||
/// If `None` is provided, egui will assume a time delta of `predicted_dt` (default 1/60 seconds).
|
||||
pub time: Option<f64>,
|
||||
|
||||
/// Should be set to the expected time between frames when painting at vsync speeds.
|
||||
/// The default for this is 1/60.
|
||||
/// Can safely be left at its default value.
|
||||
pub predicted_dt: f32,
|
||||
|
||||
/// Which modifier keys are down at the start of the frame?
|
||||
pub modifiers: Modifiers,
|
||||
|
||||
/// In-order events received this frame.
|
||||
///
|
||||
/// There is currently no way to know if egui handles a particular event,
|
||||
/// but you can check if egui is using the keyboard with [`crate::Context::egui_wants_keyboard_input`]
|
||||
/// and/or the pointer (mouse/touch) with [`crate::Context::egui_is_using_pointer`].
|
||||
pub events: Vec<Event>,
|
||||
|
||||
/// Dragged files hovering over egui.
|
||||
pub hovered_files: Vec<HoveredFile>,
|
||||
|
||||
/// Dragged files dropped into egui.
|
||||
///
|
||||
/// Note: when using `eframe` on Windows, this will always be empty if drag-and-drop support has
|
||||
/// been disabled in [`crate::viewport::ViewportBuilder`].
|
||||
pub dropped_files: Vec<DroppedFile>,
|
||||
|
||||
/// The native window has the keyboard focus (i.e. is receiving key presses).
|
||||
///
|
||||
/// False when the user alt-tab away from the application, for instance.
|
||||
pub focused: bool,
|
||||
|
||||
/// Does the OS use dark or light mode?
|
||||
///
|
||||
/// `None` means "don't know".
|
||||
pub system_theme: Option<Theme>,
|
||||
}
|
||||
|
||||
impl Default for RawInput {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
viewport_id: ViewportId::ROOT,
|
||||
viewports: std::iter::once((ViewportId::ROOT, Default::default())).collect(),
|
||||
screen_rect: None,
|
||||
max_texture_side: None,
|
||||
time: None,
|
||||
predicted_dt: 1.0 / 60.0,
|
||||
modifiers: Modifiers::default(),
|
||||
events: vec![],
|
||||
hovered_files: Default::default(),
|
||||
dropped_files: Default::default(),
|
||||
focused: true, // integrations opt into global focus tracking
|
||||
system_theme: None,
|
||||
safe_area_insets: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RawInput {
|
||||
/// Info about the active viewport
|
||||
#[inline]
|
||||
pub fn viewport(&self) -> &ViewportInfo {
|
||||
self.viewports.get(&self.viewport_id).expect("Failed to find current viewport in egui RawInput. This is the fault of the egui backend")
|
||||
}
|
||||
|
||||
/// Helper: move volatile (deltas and events), clone the rest.
|
||||
///
|
||||
/// * [`Self::hovered_files`] is cloned.
|
||||
/// * [`Self::dropped_files`] is moved.
|
||||
pub fn take(&mut self) -> Self {
|
||||
Self {
|
||||
viewport_id: self.viewport_id,
|
||||
viewports: self
|
||||
.viewports
|
||||
.iter_mut()
|
||||
.map(|(id, info)| (*id, info.take()))
|
||||
.collect(),
|
||||
screen_rect: self.screen_rect.take(),
|
||||
safe_area_insets: self.safe_area_insets.take(),
|
||||
max_texture_side: self.max_texture_side.take(),
|
||||
time: self.time,
|
||||
predicted_dt: self.predicted_dt,
|
||||
modifiers: self.modifiers,
|
||||
events: std::mem::take(&mut self.events),
|
||||
hovered_files: self.hovered_files.clone(),
|
||||
dropped_files: std::mem::take(&mut self.dropped_files),
|
||||
focused: self.focused,
|
||||
system_theme: self.system_theme,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add on new input.
|
||||
pub fn append(&mut self, newer: Self) {
|
||||
let Self {
|
||||
viewport_id: viewport_ids,
|
||||
viewports,
|
||||
screen_rect,
|
||||
max_texture_side,
|
||||
time,
|
||||
predicted_dt,
|
||||
modifiers,
|
||||
mut events,
|
||||
mut hovered_files,
|
||||
mut dropped_files,
|
||||
focused,
|
||||
system_theme,
|
||||
safe_area_insets: safe_area,
|
||||
} = newer;
|
||||
|
||||
self.viewport_id = viewport_ids;
|
||||
self.viewports = viewports;
|
||||
self.screen_rect = screen_rect.or(self.screen_rect);
|
||||
self.max_texture_side = max_texture_side.or(self.max_texture_side);
|
||||
self.time = time; // use latest time
|
||||
self.predicted_dt = predicted_dt; // use latest dt
|
||||
self.modifiers = modifiers; // use latest
|
||||
self.events.append(&mut events);
|
||||
self.hovered_files.append(&mut hovered_files);
|
||||
self.dropped_files.append(&mut dropped_files);
|
||||
self.focused = focused;
|
||||
self.system_theme = system_theme;
|
||||
self.safe_area_insets = safe_area;
|
||||
}
|
||||
}
|
||||
|
||||
impl RawInput {
|
||||
pub fn ui(&self, ui: &mut crate::Ui) {
|
||||
let Self {
|
||||
viewport_id,
|
||||
viewports,
|
||||
screen_rect,
|
||||
max_texture_side,
|
||||
time,
|
||||
predicted_dt,
|
||||
modifiers,
|
||||
events,
|
||||
hovered_files,
|
||||
dropped_files,
|
||||
focused,
|
||||
system_theme,
|
||||
safe_area_insets: safe_area,
|
||||
} = self;
|
||||
|
||||
ui.label(format!("Active viewport: {viewport_id:?}"));
|
||||
let ordered_viewports = viewports
|
||||
.iter()
|
||||
.map(|(id, value)| (*id, value))
|
||||
.collect::<OrderedViewportIdMap<_>>();
|
||||
for (id, viewport) in ordered_viewports {
|
||||
ui.group(|ui| {
|
||||
ui.label(format!("Viewport {id:?}"));
|
||||
ui.push_id(id, |ui| {
|
||||
viewport.ui(ui);
|
||||
});
|
||||
});
|
||||
}
|
||||
ui.label(format!("screen_rect: {screen_rect:?} points"));
|
||||
|
||||
ui.label(format!("max_texture_side: {max_texture_side:?}"));
|
||||
if let Some(time) = time {
|
||||
ui.label(format!("time: {time:.3} s"));
|
||||
} else {
|
||||
ui.label("time: None");
|
||||
}
|
||||
ui.label(format!("predicted_dt: {:.1} ms", 1e3 * predicted_dt));
|
||||
ui.label(format!("modifiers: {modifiers:#?}"));
|
||||
ui.label(format!("hovered_files: {}", hovered_files.len()));
|
||||
ui.label(format!("dropped_files: {}", dropped_files.len()));
|
||||
ui.label(format!("focused: {focused}"));
|
||||
ui.label(format!("system_theme: {system_theme:?}"));
|
||||
ui.label(format!("safe_area: {safe_area:?}"));
|
||||
ui.scope(|ui| {
|
||||
ui.set_min_height(150.0);
|
||||
ui.label(format!("events: {events:#?}"))
|
||||
.on_hover_text("key presses etc");
|
||||
});
|
||||
}
|
||||
}
|
||||
19
crates/egui/src/data/input/safe_area_insets.rs
Normal file
19
crates/egui/src/data/input/safe_area_insets.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use epaint::MarginF32;
|
||||
|
||||
use crate::emath::Rect;
|
||||
|
||||
/// The 'safe area' insets of the screen
|
||||
///
|
||||
/// This represents the area taken up by the status bar, navigation controls, notches,
|
||||
/// or any other items that obscure parts of the screen.
|
||||
#[derive(Debug, PartialEq, Copy, Clone, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct SafeAreaInsets(pub MarginF32);
|
||||
|
||||
impl std::ops::Sub<SafeAreaInsets> for Rect {
|
||||
type Output = Self;
|
||||
|
||||
fn sub(self, rhs: SafeAreaInsets) -> Self::Output {
|
||||
self - rhs.0
|
||||
}
|
||||
}
|
||||
50
crates/egui/src/data/input/touch.rs
Normal file
50
crates/egui/src/data/input/touch.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
/// this is a `u64` as values of this kind can always be obtained by hashing
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct TouchDeviceId(pub u64);
|
||||
|
||||
/// Unique identification of a touch occurrence (finger or pen or …).
|
||||
/// A Touch ID is valid until the finger is lifted.
|
||||
/// A new ID is used for the next touch.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct TouchId(pub u64);
|
||||
|
||||
/// In what phase a touch event is in.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub enum TouchPhase {
|
||||
/// User just placed a touch point on the touch surface
|
||||
Start,
|
||||
|
||||
/// User moves a touch point along the surface. This event is also sent when
|
||||
/// any attributes (position, force, …) of the touch point change.
|
||||
Move,
|
||||
|
||||
/// User lifted the finger or pen from the surface, or slid off the edge of
|
||||
/// the surface
|
||||
End,
|
||||
|
||||
/// Touch operation has been disrupted by something (various reasons are possible,
|
||||
/// maybe a pop-up alert or any other kind of interruption which may not have
|
||||
/// been intended by the user)
|
||||
Cancel,
|
||||
}
|
||||
|
||||
impl From<u64> for TouchId {
|
||||
fn from(id: u64) -> Self {
|
||||
Self(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i32> for TouchId {
|
||||
fn from(id: i32) -> Self {
|
||||
Self(id as u64)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u32> for TouchId {
|
||||
fn from(id: u32) -> Self {
|
||||
Self(id as u64)
|
||||
}
|
||||
}
|
||||
217
crates/egui/src/data/input/viewport_info.rs
Normal file
217
crates/egui/src/data/input/viewport_info.rs
Normal file
@@ -0,0 +1,217 @@
|
||||
use crate::emath::{Rect, Vec2};
|
||||
|
||||
/// An input event from the backend into egui, about a specific [viewport](crate::viewport).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub enum ViewportEvent {
|
||||
/// The user clicked the close-button on the window, or similar.
|
||||
///
|
||||
/// If this is the root viewport, the application will exit
|
||||
/// after this frame unless you send a
|
||||
/// [`crate::ViewportCommand::CancelClose`] command.
|
||||
///
|
||||
/// If this is not the root viewport,
|
||||
/// it is up to the user to hide this viewport the next frame.
|
||||
///
|
||||
/// This even will wake up both the child and parent viewport.
|
||||
Close,
|
||||
}
|
||||
|
||||
/// Information about the current viewport, given as input each frame.
|
||||
///
|
||||
/// `None` means "unknown".
|
||||
///
|
||||
/// All units are in ui "points", which can be calculated from native physical pixels
|
||||
/// using `pixels_per_point` = [`crate::Context::zoom_factor`] * `[Self::native_pixels_per_point`];
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct ViewportInfo {
|
||||
/// Parent viewport, if known.
|
||||
pub parent: Option<crate::ViewportId>,
|
||||
|
||||
/// Name of the viewport, if known.
|
||||
pub title: Option<String>,
|
||||
|
||||
pub events: Vec<ViewportEvent>,
|
||||
|
||||
/// The OS native pixels-per-point.
|
||||
///
|
||||
/// This should always be set, if known.
|
||||
///
|
||||
/// On web this takes browser scaling into account,
|
||||
/// and corresponds to [`window.devicePixelRatio`](https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio) in JavaScript.
|
||||
pub native_pixels_per_point: Option<f32>,
|
||||
|
||||
/// Current monitor size in egui points.
|
||||
pub monitor_size: Option<Vec2>,
|
||||
|
||||
/// The inner rectangle of the native window, in monitor space and ui points scale.
|
||||
///
|
||||
/// This is the content rectangle of the viewport.
|
||||
///
|
||||
/// **`eframe` notes**:
|
||||
///
|
||||
/// On Android / Wayland, this will always be `None` since getting the
|
||||
/// position of the window is not possible.
|
||||
pub inner_rect: Option<Rect>,
|
||||
|
||||
/// The outer rectangle of the native window, in monitor space and ui points scale.
|
||||
///
|
||||
/// This is the content rectangle plus decoration chrome.
|
||||
///
|
||||
/// **`eframe` notes**:
|
||||
///
|
||||
/// On Android / Wayland, this will always be `None` since getting the
|
||||
/// position of the window is not possible.
|
||||
pub outer_rect: Option<Rect>,
|
||||
|
||||
/// Are we minimized?
|
||||
pub minimized: Option<bool>,
|
||||
|
||||
/// Are we maximized?
|
||||
pub maximized: Option<bool>,
|
||||
|
||||
/// Are we in fullscreen mode?
|
||||
pub fullscreen: Option<bool>,
|
||||
|
||||
/// Is the window focused and able to receive input?
|
||||
///
|
||||
/// This should be the same as [`RawInput::focused`](crate::RawInput::focused).
|
||||
pub focused: Option<bool>,
|
||||
|
||||
/// Is the window fully occluded (completely covered) by another window?
|
||||
///
|
||||
/// Not all platforms support this.
|
||||
/// On platforms that don't, this will be `None` or `Some(false)`.
|
||||
pub occluded: Option<bool>,
|
||||
}
|
||||
|
||||
impl ViewportInfo {
|
||||
/// Is the window considered visible for rendering purposes?
|
||||
///
|
||||
/// A window is not visible if it is minimized or occluded.
|
||||
/// When not visible, the UI is not painted and rendering is skipped,
|
||||
/// but application logic may still be executed by some integrations.
|
||||
pub fn visible(&self) -> Option<bool> {
|
||||
match (self.minimized, self.occluded) {
|
||||
(Some(true), _) | (_, Some(true)) => Some(false),
|
||||
(Some(false), Some(false)) => Some(true),
|
||||
(_, None) | (None, _) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// This viewport has been told to close.
|
||||
///
|
||||
/// If this is the root viewport, the application will exit
|
||||
/// after this frame unless you send a
|
||||
/// [`crate::ViewportCommand::CancelClose`] command.
|
||||
///
|
||||
/// If this is not the root viewport,
|
||||
/// it is up to the user to hide this viewport the next frame.
|
||||
pub fn close_requested(&self) -> bool {
|
||||
self.events.contains(&ViewportEvent::Close)
|
||||
}
|
||||
|
||||
/// Helper: move [`Self::events`], clone the other fields.
|
||||
pub fn take(&mut self) -> Self {
|
||||
Self {
|
||||
parent: self.parent,
|
||||
title: self.title.clone(),
|
||||
events: std::mem::take(&mut self.events),
|
||||
native_pixels_per_point: self.native_pixels_per_point,
|
||||
monitor_size: self.monitor_size,
|
||||
inner_rect: self.inner_rect,
|
||||
outer_rect: self.outer_rect,
|
||||
minimized: self.minimized,
|
||||
maximized: self.maximized,
|
||||
fullscreen: self.fullscreen,
|
||||
focused: self.focused,
|
||||
occluded: self.occluded,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ui(&self, ui: &mut crate::Ui) {
|
||||
let Self {
|
||||
parent,
|
||||
title,
|
||||
events,
|
||||
native_pixels_per_point,
|
||||
monitor_size,
|
||||
inner_rect,
|
||||
outer_rect,
|
||||
minimized,
|
||||
maximized,
|
||||
fullscreen,
|
||||
focused,
|
||||
occluded,
|
||||
} = self;
|
||||
|
||||
crate::Grid::new("viewport_info").show(ui, |ui| {
|
||||
ui.label("Parent:");
|
||||
ui.label(opt_as_str(parent));
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Title:");
|
||||
ui.label(opt_as_str(title));
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Events:");
|
||||
ui.label(format!("{events:?}"));
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Native pixels-per-point:");
|
||||
ui.label(opt_as_str(native_pixels_per_point));
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Monitor size:");
|
||||
ui.label(opt_as_str(monitor_size));
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Inner rect:");
|
||||
ui.label(opt_rect_as_string(inner_rect));
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Outer rect:");
|
||||
ui.label(opt_rect_as_string(outer_rect));
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Minimized:");
|
||||
ui.label(opt_as_str(minimized));
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Maximized:");
|
||||
ui.label(opt_as_str(maximized));
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Fullscreen:");
|
||||
ui.label(opt_as_str(fullscreen));
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Focused:");
|
||||
ui.label(opt_as_str(focused));
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Occluded:");
|
||||
ui.label(opt_as_str(occluded));
|
||||
ui.end_row();
|
||||
|
||||
let visible = self.visible();
|
||||
|
||||
ui.label("Visible:");
|
||||
ui.label(opt_as_str(&visible));
|
||||
ui.end_row();
|
||||
|
||||
#[expect(clippy::ref_option)]
|
||||
fn opt_rect_as_string(v: &Option<Rect>) -> String {
|
||||
v.as_ref().map_or(String::new(), |r| {
|
||||
format!("Pos: {:?}, size: {:?}", r.min, r.size())
|
||||
})
|
||||
}
|
||||
|
||||
#[expect(clippy::ref_option)]
|
||||
fn opt_as_str<T: std::fmt::Debug>(v: &Option<T>) -> String {
|
||||
v.as_ref().map_or(String::new(), |v| format!("{v:?}"))
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -188,6 +188,38 @@ pub enum Key {
|
||||
/// Android sends this key on Back button press.
|
||||
/// Does not work on Web.
|
||||
BrowserBack,
|
||||
|
||||
// ----------------------------------------------
|
||||
// Modifier keys (exposed as distinct left/right variants so that
|
||||
// games and input-capture UIs can bind them independently). egui's
|
||||
// `Modifiers` struct still collapses both sides for the common case
|
||||
// (e.g. "Ctrl+C"); these variants are emitted only as physical
|
||||
// `Event::Key` presses.
|
||||
/// Left Shift key.
|
||||
ShiftLeft,
|
||||
/// Right Shift key.
|
||||
ShiftRight,
|
||||
/// Left Control key.
|
||||
ControlLeft,
|
||||
/// Right Control key.
|
||||
ControlRight,
|
||||
/// Left Alt / Option key.
|
||||
AltLeft,
|
||||
/// Right Alt / AltGr / Option key.
|
||||
AltRight,
|
||||
/// Left Super / Meta / Command / Windows key.
|
||||
SuperLeft,
|
||||
/// Right Super / Meta / Command / Windows key.
|
||||
SuperRight,
|
||||
|
||||
// ----------------------------------------------
|
||||
// International keys — physical positions that only exist on
|
||||
// non-US keyboards.
|
||||
/// ISO 102nd key: physically located between the left Shift and Z
|
||||
/// on ISO layouts. On French AZERTY it produces `<>|`; on UK
|
||||
/// QWERTY a secondary `\` / `|`. Missing from US ANSI keyboards.
|
||||
IntlBackslash,
|
||||
|
||||
// When adding keys, remember to also update:
|
||||
// * crates/egui-winit/src/lib.rs
|
||||
// * Key::ALL
|
||||
@@ -314,6 +346,17 @@ impl Key {
|
||||
Self::F35,
|
||||
// Navigation keys:
|
||||
Self::BrowserBack,
|
||||
// Modifier keys (physical L/R):
|
||||
Self::ShiftLeft,
|
||||
Self::ShiftRight,
|
||||
Self::ControlLeft,
|
||||
Self::ControlRight,
|
||||
Self::AltLeft,
|
||||
Self::AltRight,
|
||||
Self::SuperLeft,
|
||||
Self::SuperRight,
|
||||
// International keys:
|
||||
Self::IntlBackslash,
|
||||
];
|
||||
|
||||
/// Converts `"A"` to `Key::A`, `Space` to `Key::Space`, etc.
|
||||
@@ -444,6 +487,17 @@ impl Key {
|
||||
|
||||
"BrowserBack" => Self::BrowserBack,
|
||||
|
||||
"ShiftLeft" => Self::ShiftLeft,
|
||||
"ShiftRight" => Self::ShiftRight,
|
||||
"ControlLeft" => Self::ControlLeft,
|
||||
"ControlRight" => Self::ControlRight,
|
||||
"AltLeft" => Self::AltLeft,
|
||||
"AltRight" => Self::AltRight,
|
||||
"SuperLeft" => Self::SuperLeft,
|
||||
"SuperRight" => Self::SuperRight,
|
||||
|
||||
"IntlBackslash" => Self::IntlBackslash,
|
||||
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
@@ -599,6 +653,17 @@ impl Key {
|
||||
Self::F35 => "F35",
|
||||
|
||||
Self::BrowserBack => "BrowserBack",
|
||||
|
||||
Self::ShiftLeft => "ShiftLeft",
|
||||
Self::ShiftRight => "ShiftRight",
|
||||
Self::ControlLeft => "ControlLeft",
|
||||
Self::ControlRight => "ControlRight",
|
||||
Self::AltLeft => "AltLeft",
|
||||
Self::AltRight => "AltRight",
|
||||
Self::SuperLeft => "SuperLeft",
|
||||
Self::SuperRight => "SuperRight",
|
||||
|
||||
Self::IntlBackslash => "IntlBackslash",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -607,7 +672,7 @@ impl Key {
|
||||
fn test_key_from_name() {
|
||||
assert_eq!(
|
||||
Key::ALL.len(),
|
||||
Key::BrowserBack as usize + 1,
|
||||
Key::IntlBackslash as usize + 1,
|
||||
"Some keys are missing in Key::ALL"
|
||||
);
|
||||
|
||||
|
||||
@@ -65,7 +65,12 @@ impl Id {
|
||||
|
||||
/// Generate a new root [`Id`] by hashing some source (e.g. a string or integer).
|
||||
pub fn new(source: impl AsId) -> Self {
|
||||
Self::from_hash(ahash::RandomState::with_seeds(1, 2, 3, 4).hash_one(source))
|
||||
let id = Self::from_hash(ahash::RandomState::with_seeds(1, 2, 3, 4).hash_one(&source));
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
id_source::insert_root(id, &source);
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
/// Generate a child [`Id`] by salting the parent [`Id`] with the given argument.
|
||||
@@ -73,8 +78,13 @@ impl Id {
|
||||
use std::hash::{BuildHasher as _, Hasher as _};
|
||||
let mut hasher = ahash::RandomState::with_seeds(1, 2, 3, 4).build_hasher();
|
||||
hasher.write_u64(self.value());
|
||||
hasher.write_u64(IdSalt::new(salt).value());
|
||||
Self::from_hash(hasher.finish())
|
||||
hasher.write_u64(IdSalt::new(&salt).value());
|
||||
let id = Self::from_hash(hasher.finish());
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
id_source::insert_child(id, self, &salt);
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
/// Short and readable summary
|
||||
@@ -116,10 +126,19 @@ impl Id {
|
||||
|
||||
impl std::fmt::Debug for Id {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:04X}", self.value() as u16)
|
||||
if *self == Self::NULL {
|
||||
return write!(f, "Id::NULL");
|
||||
}
|
||||
#[cfg(debug_assertions)]
|
||||
if let Some(source) = id_source::get(*self) {
|
||||
return f.write_str(&source);
|
||||
}
|
||||
write!(f, "id_{:04X}", self.value() as u16)
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Convenience
|
||||
impl From<&'static str> for Id {
|
||||
#[inline]
|
||||
@@ -135,12 +154,6 @@ impl From<String> for Id {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn id_size() {
|
||||
assert_eq!(std::mem::size_of::<Id>(), 8);
|
||||
assert_eq!(std::mem::size_of::<Option<Id>>(), 8);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// `IdSet` is a `HashSet<Id>` optimized by knowing that [`Id`] has good entropy, and doesn't need more hashing.
|
||||
@@ -148,3 +161,108 @@ pub type IdSet = nohash_hasher::IntSet<Id>;
|
||||
|
||||
/// `IdMap<V>` is a `HashMap<Id, V>` optimized by knowing that [`Id`] has good entropy, and doesn't need more hashing.
|
||||
pub type IdMap<V> = nohash_hasher::IntMap<Id, V>;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// In debug builds, remember the `Debug`-formatted call chain that produced each [`Id`].
|
||||
///
|
||||
/// Used by [`Id`]'s `Debug` impl so that `Id::new("foo")` prints as `Id::new("foo")`,
|
||||
/// and `Id::new("foo").with("bar")` prints as `Id::new("foo").with("bar")`, etc.
|
||||
#[cfg(debug_assertions)]
|
||||
mod id_source {
|
||||
use super::{AsId, AsIdSalt, Id, IdMap};
|
||||
use epaint::mutex::RwLock;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
static SOURCE_MAP: LazyLock<RwLock<IdMap<String>>> = LazyLock::new(RwLock::default);
|
||||
|
||||
pub(super) fn insert_root(id: Id, source: &impl AsId) {
|
||||
if SOURCE_MAP.read().contains_key(&id) {
|
||||
return;
|
||||
}
|
||||
// Format outside the lock since `{source:?}` may itself recurse into [`Id`]'s `Debug` impl.
|
||||
let formatted = format!("Id::new({source:?})");
|
||||
SOURCE_MAP.write().insert(id, formatted);
|
||||
}
|
||||
|
||||
pub(super) fn insert_child(id: Id, parent: Id, salt: &impl AsIdSalt) {
|
||||
if SOURCE_MAP.read().contains_key(&id) {
|
||||
return;
|
||||
}
|
||||
// Look up parent's repr and drop the read guard before formatting,
|
||||
// since `{parent:?}` and `{salt:?}` may themselves recurse into [`Id`]'s `Debug` impl.
|
||||
let cached_parent_repr = SOURCE_MAP.read().get(&parent).cloned();
|
||||
let parent_repr = cached_parent_repr.unwrap_or_else(|| format!("{parent:?}"));
|
||||
let formatted = format!("{parent_repr}.with({salt:?})");
|
||||
SOURCE_MAP.write().insert(id, formatted);
|
||||
}
|
||||
|
||||
pub(super) fn get(id: Id) -> Option<String> {
|
||||
SOURCE_MAP.read().get(&id).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn id_size() {
|
||||
assert_eq!(std::mem::size_of::<Id>(), 8);
|
||||
assert_eq!(std::mem::size_of::<Option<Id>>(), 8);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(debug_assertions)]
|
||||
mod debug_format_tests {
|
||||
use crate::IdSalt;
|
||||
|
||||
use super::Id;
|
||||
|
||||
#[test]
|
||||
fn root_string() {
|
||||
let id = Id::new("foo");
|
||||
assert_eq!(format!("{id:?}"), r#"Id::new("foo")"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn root_integer() {
|
||||
let id = Id::new(42_i32);
|
||||
assert_eq!(format!("{id:?}"), "Id::new(42)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn root_id_salt() {
|
||||
let id = Id::new(IdSalt::new("foo"));
|
||||
assert_eq!(format!("{id:?}"), r#"Id::new(IdSalt::new("foo"))"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_one_child() {
|
||||
let id = Id::new("parent").with("child");
|
||||
assert_eq!(format!("{id:?}"), r#"Id::new("parent").with("child")"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_chain() {
|
||||
let id = Id::new("a").with("b").with("c").with(7_i32);
|
||||
assert_eq!(
|
||||
format!("{id:?}"),
|
||||
r#"Id::new("a").with("b").with("c").with(7)"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_id_as_source() {
|
||||
let inner = Id::new("foo");
|
||||
let outer = Id::new(inner);
|
||||
assert_eq!(format!("{outer:?}"), r#"Id::new(Id::new("foo"))"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn null_prints_as_null() {
|
||||
assert_eq!(format!("{:?}", Id::NULL), "Id::NULL");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn null_as_parent() {
|
||||
let id = Id::NULL.with("foo");
|
||||
assert_eq!(format!("{id:?}"), r#"Id::NULL.with("foo")"#);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,12 @@ impl nohash_hasher::IsEnabled for IdSalt {}
|
||||
impl IdSalt {
|
||||
/// Create a new [`IdSalt`] by hashing some source (e.g. a string or integer).
|
||||
pub fn new(source: impl AsIdSalt) -> Self {
|
||||
Self::from_hash(ahash::RandomState::with_seeds(1, 2, 3, 4).hash_one(source))
|
||||
let id_salt = Self::from_hash(ahash::RandomState::with_seeds(5, 6, 7, 8).hash_one(&source));
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
id_salt_source::maybe_insert(id_salt, &source);
|
||||
|
||||
id_salt
|
||||
}
|
||||
|
||||
/// Create a new root [`IdSalt`] from a high-entropy hash.
|
||||
@@ -54,6 +59,78 @@ impl IdSalt {
|
||||
|
||||
impl std::fmt::Debug for IdSalt {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
#[cfg(debug_assertions)]
|
||||
if let Some(source) = id_salt_source::get(*self) {
|
||||
return write!(f, "IdSalt::new({source})");
|
||||
}
|
||||
write!(f, "salt_{:04X}", self.value() as u16)
|
||||
}
|
||||
}
|
||||
|
||||
/// In debug builds, remember the `Debug`-formatted source that produced each [`IdSalt`].
|
||||
///
|
||||
/// Used by [`IdSalt`]'s `Debug` impl so that `IdSalt::new("foo")` prints as
|
||||
/// `IdSalt::new("foo")`, and `IdSalt::new(IdSalt::new("foo"))` prints as
|
||||
/// `IdSalt::new(IdSalt::new("foo"))`, etc.
|
||||
#[cfg(debug_assertions)]
|
||||
mod id_salt_source {
|
||||
use super::{AsIdSalt, IdSalt};
|
||||
use epaint::mutex::RwLock;
|
||||
use nohash_hasher::IntMap;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
static SOURCE_MAP: LazyLock<RwLock<IntMap<IdSalt, String>>> = LazyLock::new(RwLock::default);
|
||||
|
||||
pub(super) fn maybe_insert(id_salt: IdSalt, source: &impl AsIdSalt) {
|
||||
if !SOURCE_MAP.read().contains_key(&id_salt) {
|
||||
let formatted = format!("{source:?}");
|
||||
SOURCE_MAP.write().insert(id_salt, formatted);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn get(id_salt: IdSalt) -> Option<String> {
|
||||
SOURCE_MAP.read().get(&id_salt).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(debug_assertions)]
|
||||
mod tests {
|
||||
use super::IdSalt;
|
||||
|
||||
#[test]
|
||||
fn debug_format_string_source() {
|
||||
let salt = IdSalt::new("foo");
|
||||
assert_eq!(format!("{salt:?}"), r#"IdSalt::new("foo")"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug_format_integer_source() {
|
||||
let salt = IdSalt::new(42_i32);
|
||||
assert_eq!(format!("{salt:?}"), "IdSalt::new(42)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug_format_nested_salt() {
|
||||
let inner = IdSalt::new("foo");
|
||||
let outer = IdSalt::new(inner);
|
||||
assert_eq!(format!("{outer:?}"), r#"IdSalt::new(IdSalt::new("foo"))"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug_format_triple_nested_salt() {
|
||||
let a = IdSalt::new("foo");
|
||||
let b = IdSalt::new(a);
|
||||
let c = IdSalt::new(b);
|
||||
assert_eq!(
|
||||
format!("{c:?}"),
|
||||
r#"IdSalt::new(IdSalt::new(IdSalt::new("foo")))"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug_format_tuple_source() {
|
||||
let salt = IdSalt::new(("foo", 7_i32));
|
||||
assert_eq!(format!("{salt:?}"), r#"IdSalt::new(("foo", 7))"#);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -490,6 +490,13 @@ pub(crate) struct Focus {
|
||||
/// The ID of a widget that had keyboard focus during the previous frame.
|
||||
id_previous_frame: Option<Id>,
|
||||
|
||||
/// The ID of a widget that had keyboard focus *two* frames ago.
|
||||
///
|
||||
/// Kept so `Response::lost_focus` can still fire after a mid-frame
|
||||
/// focus transition (e.g. clicking a `TextEdit` that was added to
|
||||
/// the UI later than the currently focused one).
|
||||
id_two_frames_ago: Option<Id>,
|
||||
|
||||
/// The ID of a widget to give the focus to in the next frame.
|
||||
id_next_frame: Option<Id>,
|
||||
|
||||
@@ -545,6 +552,7 @@ impl Focus {
|
||||
}
|
||||
|
||||
fn begin_pass(&mut self, new_input: &crate::data::input::RawInput) {
|
||||
self.id_two_frames_ago = self.id_previous_frame;
|
||||
self.id_previous_frame = self.focused();
|
||||
if let Some(id) = self.id_next_frame.take() {
|
||||
self.focused_widget = Some(FocusWidget::new(id));
|
||||
@@ -831,10 +839,21 @@ impl Memory {
|
||||
self.focus().and_then(|f| f.id_previous_frame) == Some(id)
|
||||
}
|
||||
|
||||
/// Check if the layer lost focus last frame.
|
||||
/// returns `true` if the layer lost focus last frame, but not this one.
|
||||
/// Check if the widget lost keyboard focus.
|
||||
///
|
||||
/// Returns `true` when `id` was the focused widget at the start
|
||||
/// of this frame *or* the start of the previous frame — but is
|
||||
/// not focused now. The two-frame window matters when focus
|
||||
/// transfers mid-frame: the previously-focused widget has
|
||||
/// usually already been rendered by the time another widget
|
||||
/// claims focus, so the loss signal can only reach it on its
|
||||
/// next render pass.
|
||||
pub(crate) fn lost_focus(&self, id: Id) -> bool {
|
||||
self.had_focus_last_frame(id) && !self.has_focus(id)
|
||||
let had_recent_focus = self
|
||||
.focus()
|
||||
.map(|f| f.id_previous_frame == Some(id) || f.id_two_frames_ago == Some(id))
|
||||
.unwrap_or(false);
|
||||
had_recent_focus && !self.has_focus(id)
|
||||
}
|
||||
|
||||
/// Check if the layer gained focus this frame.
|
||||
@@ -1368,6 +1387,54 @@ fn memory_impl_send_sync() {
|
||||
assert_send_sync::<Memory>();
|
||||
}
|
||||
|
||||
// Regression test for https://github.com/emilk/egui/issues/2142.
|
||||
#[test]
|
||||
fn lost_focus_fires_after_mid_frame_focus_transfer() {
|
||||
use crate::data::input::RawInput;
|
||||
let a = Id::new("A");
|
||||
let b = Id::new("B");
|
||||
let mut focus = Focus::default();
|
||||
let raw = RawInput::default();
|
||||
|
||||
fn lost_focus_check(focus: &Focus, id: Id) -> bool {
|
||||
let was_focused =
|
||||
focus.id_previous_frame == Some(id) || focus.id_two_frames_ago == Some(id);
|
||||
was_focused && focus.focused() != Some(id)
|
||||
}
|
||||
|
||||
// Frame N-1
|
||||
{
|
||||
focus.begin_pass(&raw);
|
||||
focus.focused_widget = Some(FocusWidget::new(a));
|
||||
}
|
||||
|
||||
// Frame N: `A` is focused at start; user clicks `B` mid-frame
|
||||
{
|
||||
focus.begin_pass(&raw);
|
||||
assert_eq!(focus.id_previous_frame, Some(a));
|
||||
assert!(!lost_focus_check(&focus, a));
|
||||
focus.focused_widget = Some(FocusWidget::new(b));
|
||||
}
|
||||
|
||||
// Frame N+1: `A` deferred lost_focus signal must fire
|
||||
{
|
||||
focus.begin_pass(&raw);
|
||||
assert_eq!(focus.id_two_frames_ago, Some(a));
|
||||
assert_eq!(focus.id_previous_frame, Some(b));
|
||||
assert!(lost_focus_check(&focus, a), "`A` lost_focus must fire");
|
||||
assert!(!lost_focus_check(&focus, b));
|
||||
}
|
||||
|
||||
// Frame N+2
|
||||
{
|
||||
focus.begin_pass(&raw);
|
||||
assert!(
|
||||
!lost_focus_check(&focus, a),
|
||||
"A's lost_focus must stop firing once the two-frame window passes",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn order_map_total_ordering() {
|
||||
let mut layers = [
|
||||
|
||||
@@ -117,6 +117,8 @@ impl UiBuilder {
|
||||
/// Make the new `Ui` disabled, i.e. grayed-out and non-interactive.
|
||||
///
|
||||
/// Note that if the parent `Ui` is disabled, the child will always be disabled.
|
||||
///
|
||||
/// See also [`crate::Ui::add_enabled`], [`crate::Ui::add_enabled_ui`] and [`crate::Ui::is_enabled`].
|
||||
#[inline]
|
||||
pub fn disabled(mut self) -> Self {
|
||||
self.disabled = true;
|
||||
|
||||
@@ -85,7 +85,29 @@ pub fn show_color_at(painter: &Painter, color: Color32, rect: Rect) {
|
||||
}
|
||||
}
|
||||
|
||||
fn color_button(ui: &mut Ui, color: Color32, open: bool) -> Response {
|
||||
/// Show a color with background checkers to demonstrate transparency (if any).
|
||||
fn show_srgba_unmultiplied(ui: &mut Ui, srgba: [u8; 4], desired_size: Vec2) -> Response {
|
||||
let (rect, response) = ui.allocate_at_least(desired_size, Sense::hover());
|
||||
if ui.is_rect_visible(rect) {
|
||||
show_srgba_unmultiplied_at(ui.painter(), srgba, rect);
|
||||
}
|
||||
response
|
||||
}
|
||||
|
||||
/// Show a color with background checkers to demonstrate transparency (if any).
|
||||
fn show_srgba_unmultiplied_at(painter: &Painter, [r, g, b, a]: [u8; 4], rect: Rect) {
|
||||
if a == 255 {
|
||||
painter.rect_filled(rect, 0.0, Color32::from_rgb(r, g, b));
|
||||
} else {
|
||||
background_checkers(painter, rect);
|
||||
let left = Rect::from_min_max(rect.left_top(), rect.center_bottom());
|
||||
let right = Rect::from_min_max(rect.center_top(), rect.right_bottom());
|
||||
painter.rect_filled(left, 0.0, Color32::from_rgba_unmultiplied(r, g, b, a));
|
||||
painter.rect_filled(right, 0.0, Color32::from_rgb(r, g, b));
|
||||
}
|
||||
}
|
||||
|
||||
fn color_button(ui: &mut Ui, srgba: [u8; 4], open: bool) -> Response {
|
||||
let size = ui.spacing().interact_size;
|
||||
let (rect, response) = ui.allocate_exact_size(size, Sense::click());
|
||||
response.widget_info(|| WidgetInfo::new(WidgetType::ColorButton));
|
||||
@@ -99,7 +121,7 @@ fn color_button(ui: &mut Ui, color: Color32, open: bool) -> Response {
|
||||
let rect = rect.expand(visuals.expansion);
|
||||
|
||||
let stroke_width = 1.0;
|
||||
show_color_at(ui.painter(), color, rect.shrink(stroke_width));
|
||||
show_srgba_unmultiplied_at(ui.painter(), srgba, rect.shrink(stroke_width));
|
||||
|
||||
let corner_radius = visuals.corner_radius.at_most(2); // Can't do more rounding because the background grid doesn't do any rounding
|
||||
ui.painter().rect_stroke(
|
||||
@@ -314,7 +336,12 @@ fn color_picker_hsvag_2d(ui: &mut Ui, hsvag: &mut HsvaGamma, alpha: Alpha) {
|
||||
}
|
||||
|
||||
let current_color_size = vec2(ui.spacing().slider_width, ui.spacing().interact_size.y);
|
||||
show_color(ui, *hsvag, current_color_size).on_hover_text("Selected color");
|
||||
show_srgba_unmultiplied(
|
||||
ui,
|
||||
Hsva::from(*hsvag).to_srgba_unmultiplied(),
|
||||
current_color_size,
|
||||
)
|
||||
.on_hover_text("Selected color");
|
||||
|
||||
if alpha == Alpha::BlendOrAdditive {
|
||||
let a = &mut hsvag.a;
|
||||
@@ -491,7 +518,7 @@ pub fn color_picker_color32(ui: &mut Ui, srgba: &mut Color32, alpha: Alpha) -> b
|
||||
pub fn color_edit_button_hsva(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) -> Response {
|
||||
let popup_id = ui.auto_id_with("popup");
|
||||
let open = Popup::is_id_open(ui.ctx(), popup_id);
|
||||
let mut button_response = color_button(ui, (*hsva).into(), open);
|
||||
let mut button_response = color_button(ui, hsva.to_srgba_unmultiplied(), open);
|
||||
if ui.style().explanation_tooltips {
|
||||
button_response = button_response.on_hover_text("Click to edit color");
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b9f5204a9b8f15e0f144e66f0df8685e4e3ed90cd265474f2600fdd4cb5df390
|
||||
size 98934
|
||||
oid sha256:1cf4c34af7b69cd8220b11ff7e355ddf8d7b52a43a60a1748abf9cc1d5c7da9b
|
||||
size 98869
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c9b497aa9b2b92843937c84a6ff8901f248501d5d4a89c2fb1237008a44fcad1
|
||||
size 114038
|
||||
oid sha256:bbd7fa4db7dd580968949b9d76c1521d5627cc1ee8cd19bb3dea752d3d47607b
|
||||
size 114174
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9c0bf76e2a4d60fd4ba302b2217beaa4b0627614ff7d23294c7f5e6b81a028c7
|
||||
size 45061
|
||||
oid sha256:3daf9b7cbfb48f6d083126e58c605cb19f462509acc03843f0bca5c645945e23
|
||||
size 45044
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:645519e1e4c196b985b6b422ddf1ba51e7e92d1b0c80972318dcf44dab3022d3
|
||||
size 118907
|
||||
oid sha256:4ef3a17791ca4a3e0209eec5d191c00c66e08fc4dd07fb4845490288c5bfbdb6
|
||||
size 118889
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8e1a958b753fee4bec405a74dd7727e5c477c946484ed086c5c4ffee058dd5e8
|
||||
size 35960
|
||||
oid sha256:75ff881add9f2968d19d8bb1d39239de5d99105027e91af209dfa5b45e644d2c
|
||||
size 35943
|
||||
|
||||
@@ -5,6 +5,10 @@ This file is updated upon each release.
|
||||
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
|
||||
|
||||
|
||||
## 0.34.3 - 2026-05-27
|
||||
Nothing new
|
||||
|
||||
|
||||
## 0.34.2 - 2026-05-04
|
||||
Nothing new
|
||||
|
||||
|
||||
@@ -634,7 +634,11 @@ impl TableState {
|
||||
InitialColumnSize::Automatic(_) => Size::exact(*prev_width),
|
||||
InitialColumnSize::Remainder => Size::remainder(),
|
||||
}
|
||||
.at_least(column.width_range.min.max(*max_used))
|
||||
.at_least(if column.clip {
|
||||
column.width_range.min
|
||||
} else {
|
||||
column.width_range.min.max(*max_used)
|
||||
})
|
||||
.at_most(column.width_range.max)
|
||||
};
|
||||
sizing.add(size);
|
||||
|
||||
@@ -6,6 +6,10 @@ Changes since the last release can be found at <https://github.com/emilk/egui/co
|
||||
|
||||
|
||||
|
||||
## 0.34.3 - 2026-05-27
|
||||
Nothing new
|
||||
|
||||
|
||||
## 0.34.2 - 2026-05-04
|
||||
Nothing new
|
||||
|
||||
|
||||
@@ -6,6 +6,10 @@ This file is updated upon each release.
|
||||
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
|
||||
|
||||
|
||||
## 0.34.3 - 2026-05-27
|
||||
Nothing new
|
||||
|
||||
|
||||
## 0.34.2 - 2026-05-04
|
||||
Nothing new
|
||||
|
||||
|
||||
@@ -6,6 +6,10 @@ This file is updated upon each release.
|
||||
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
|
||||
|
||||
|
||||
## 0.34.3 - 2026-05-27
|
||||
Nothing new
|
||||
|
||||
|
||||
## 0.34.2 - 2026-05-04
|
||||
Nothing new
|
||||
|
||||
|
||||
@@ -5,6 +5,10 @@ This file is updated upon each release.
|
||||
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
|
||||
|
||||
|
||||
## 0.34.3 - 2026-05-27
|
||||
Nothing new
|
||||
|
||||
|
||||
## 0.34.2 - 2026-05-04
|
||||
* Fix text layout bugs in wrapped texts [#8137](https://github.com/emilk/egui/pull/8137) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||
|
||||
|
||||
@@ -5,6 +5,10 @@ This file is updated upon each release.
|
||||
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
|
||||
|
||||
|
||||
## 0.34.3 - 2026-05-27
|
||||
Nothing new
|
||||
|
||||
|
||||
## 0.34.2 - 2026-05-04
|
||||
Nothing new
|
||||
|
||||
|
||||
Reference in New Issue
Block a user