1
0
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:
umajho
2026-06-10 19:01:02 +08:00
45 changed files with 1969 additions and 1459 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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]>>,
}

View 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>,
},
}

View 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
}
}
}

View 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,
}

View 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,
}

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

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

View 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
}
}

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

View 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,
}

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

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

View 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
}
}

View 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)
}
}

View 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:?}"))
}
});
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b9f5204a9b8f15e0f144e66f0df8685e4e3ed90cd265474f2600fdd4cb5df390
size 98934
oid sha256:1cf4c34af7b69cd8220b11ff7e355ddf8d7b52a43a60a1748abf9cc1d5c7da9b
size 98869

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c9b497aa9b2b92843937c84a6ff8901f248501d5d4a89c2fb1237008a44fcad1
size 114038
oid sha256:bbd7fa4db7dd580968949b9d76c1521d5627cc1ee8cd19bb3dea752d3d47607b
size 114174

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9c0bf76e2a4d60fd4ba302b2217beaa4b0627614ff7d23294c7f5e6b81a028c7
size 45061
oid sha256:3daf9b7cbfb48f6d083126e58c605cb19f462509acc03843f0bca5c645945e23
size 45044

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:645519e1e4c196b985b6b422ddf1ba51e7e92d1b0c80972318dcf44dab3022d3
size 118907
oid sha256:4ef3a17791ca4a3e0209eec5d191c00c66e08fc4dd07fb4845490288c5bfbdb6
size 118889

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8e1a958b753fee4bec405a74dd7727e5c477c946484ed086c5c4ffee058dd5e8
size 35960
oid sha256:75ff881add9f2968d19d8bb1d39239de5d99105027e91af209dfa5b45e644d2c
size 35943

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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