Compare commits

...

10 Commits

Author SHA1 Message Date
Skyler Lehmkuhl eb1756df3f ibus wayland fix 2026-02-16 03:36:31 -05:00
Emil Ernerfeldt 978ec6c870 Update changelogs and version for 0.33.2 2025-11-13 13:53:59 +01:00
Lucas Meurer 0ef57d5a1d Fix `ui.response().interact(Sense::click())` being flakey (#7713)
This fixes calls to `ui.response().interact(Sense::click())` being
flakey. Since egui checks widget interactions at the beginning of the
frame, based on the responses from last frame, we need to ensure that we
always call `create_widget` on `interact` calls, otherwise there can be
a feedback loop where the `Sense` egui acts on flips back and forth
between frames.

Without the fix in `interact`, both the asserts in the new test fail.

Here is a video where I experienced the bug, showing the sense switching
every frame. Every other click would fail to be detected.


https://github.com/user-attachments/assets/6be7ca0e-b50f-4d30-bf87-bbb80c319f3b

Also note, usually it's better to use `UiBuilder::sense()` to give a Ui
some sense, but sometimes you don't have the flexibility, e.g. in a `Ui`
callback from some code external to your project.
2025-11-13 13:53:59 +01:00
WickedShell d06c28cb15 Fix double negative in documentation (#7711)
The double negative of not undefined conflicted with the example given
in parens, This just removes the double negative to agree with the rest
of the doc line. I have *not* audited to see if this ordering actually
is strictly forced elsewhere. (Apologies for the smallest documentation
pull request ever)

* [x] I have followed the instructions in the PR template
2025-11-13 11:53:51 +01:00
Emil Ernerfeldt 787c467d30 Prevent widgets sometimes appearing to move relative to each other (#7710)
Sometimes when moving a window, having a tooltip attached to the mouse
pointer, or scrolling a `ScrollArea`, you would see this disturbing
effect:


![drift-bug](https://github.com/user-attachments/assets/013a5f49-ee02-417c-8441-1e1a0369e8bd)

This is caused by us rounding many visual elements (lines, rectangles,
text, …) to physical pixels in order to keep them sharp. If the
window/tooltip itself is not rounded to a physical pixel, then you can
get this behavior.

So from now on the position of all
areas/windows/tooltips/popups/ScrollArea gets rounded to the closes
pixel.

* Unlocked by https://github.com/emilk/egui/pull/7709
2025-11-13 11:53:51 +01:00
Emil Ernerfeldt 33cc8ef180 Prevent drift when resizing and moving windows (#7709)
* Follows https://github.com/emilk/egui/pull/7708
* Related to #202 

This fixes a particular issue where resizing a window would move it, and
resizing it back would not restore it.

You can still move a window by resizing it (I didn't focus on that bug
here), but at least now the window will return to its original position
when you move back the mouse.
2025-11-13 11:53:51 +01:00
Emil Ernerfeldt 5ae6d6d901 Add `Response::total_drag_delta` and `PointerState::total_drag_delta` (#7708)
Useful in many cases. In a follow-up PR I will use it to prevent drift
when dragging/resizing windows
2025-11-13 11:53:50 +01:00
Emil Ernerfeldt 74dce787af Hide scroll bars when dragging other things (#7689)
This closes a small visual glitch where scroll bars would show up when
dragging something unrelated, like a slider or a panel side.
2025-11-13 11:53:50 +01:00
Lucas Meurer d5320fe827 Improve accessibility and testability of `ComboBox` (#7658)
Changed it to use labeled_by to avoid kittest finding the label when
searching for the ComboBox and also set the value so a screen reader
will know what's selected.
2025-11-13 11:53:50 +01:00
Emil Ernerfeldt 0b9bb5f494 Make sure `native_pixels_per_point` is set during app creation (#7683)
Useful for things like analytics
2025-11-13 11:53:50 +01:00
34 changed files with 741 additions and 155 deletions

View File

@ -14,6 +14,22 @@ 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.33.2 - 2025-11-13
### ⭐ Added
* Add `Plugin::on_widget_under_pointer` to support widget inspector [#7652](https://github.com/emilk/egui/pull/7652) by [@juancampa](https://github.com/juancampa)
* Add `Response::total_drag_delta` and `PointerState::total_drag_delta` [#7708](https://github.com/emilk/egui/pull/7708) by [@emilk](https://github.com/emilk)
### 🔧 Changed
* Improve accessibility and testability of `ComboBox` [#7658](https://github.com/emilk/egui/pull/7658) by [@lucasmerlin](https://github.com/lucasmerlin)
### 🐛 Fixed
* Fix `profiling::scope` compile error when profiling using `tracing` backend [#7646](https://github.com/emilk/egui/pull/7646) by [@PPakalns](https://github.com/PPakalns)
* Fix edge cases in "smart aiming" in sliders [#7680](https://github.com/emilk/egui/pull/7680) by [@emilk](https://github.com/emilk)
* Hide scroll bars when dragging other things [#7689](https://github.com/emilk/egui/pull/7689) by [@emilk](https://github.com/emilk)
* Prevent widgets sometimes appearing to move relative to each other [#7710](https://github.com/emilk/egui/pull/7710) by [@emilk](https://github.com/emilk)
* Fix `ui.response().interact(Sense::click())` being flakey [#7713](https://github.com/emilk/egui/pull/7713) by [@lucasmerlin](https://github.com/lucasmerlin)
## 0.33.0 - 2025-10-09 - `egui::Plugin`, better kerning, kitdiff viewer
Highlights from this release:
- `egui::Plugin` a improved way to create and access egui plugins

View File

@ -1248,7 +1248,7 @@ checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53"
[[package]]
name = "ecolor"
version = "0.33.0"
version = "0.33.2"
dependencies = [
"bytemuck",
"cint",
@ -1260,7 +1260,7 @@ dependencies = [
[[package]]
name = "eframe"
version = "0.33.0"
version = "0.33.2"
dependencies = [
"ahash",
"bytemuck",
@ -1299,7 +1299,7 @@ dependencies = [
[[package]]
name = "egui"
version = "0.33.0"
version = "0.33.2"
dependencies = [
"accesskit",
"ahash",
@ -1319,7 +1319,7 @@ dependencies = [
[[package]]
name = "egui-wgpu"
version = "0.33.0"
version = "0.33.2"
dependencies = [
"ahash",
"bytemuck",
@ -1337,7 +1337,7 @@ dependencies = [
[[package]]
name = "egui-winit"
version = "0.33.0"
version = "0.33.2"
dependencies = [
"accesskit_winit",
"arboard",
@ -1360,7 +1360,7 @@ dependencies = [
[[package]]
name = "egui_demo_app"
version = "0.33.0"
version = "0.33.2"
dependencies = [
"accesskit",
"accesskit_consumer",
@ -1390,7 +1390,7 @@ dependencies = [
[[package]]
name = "egui_demo_lib"
version = "0.33.0"
version = "0.33.2"
dependencies = [
"chrono",
"criterion",
@ -1407,7 +1407,7 @@ dependencies = [
[[package]]
name = "egui_extras"
version = "0.33.0"
version = "0.33.2"
dependencies = [
"ahash",
"chrono",
@ -1426,7 +1426,7 @@ dependencies = [
[[package]]
name = "egui_glow"
version = "0.33.0"
version = "0.33.2"
dependencies = [
"bytemuck",
"document-features",
@ -1445,7 +1445,7 @@ dependencies = [
[[package]]
name = "egui_kittest"
version = "0.33.1"
version = "0.33.2"
dependencies = [
"dify",
"document-features",
@ -1463,7 +1463,7 @@ dependencies = [
[[package]]
name = "egui_tests"
version = "0.33.0"
version = "0.33.2"
dependencies = [
"egui",
"egui_extras",
@ -1493,7 +1493,7 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]]
name = "emath"
version = "0.33.0"
version = "0.33.2"
dependencies = [
"bytemuck",
"document-features",
@ -1591,7 +1591,7 @@ dependencies = [
[[package]]
name = "epaint"
version = "0.33.0"
version = "0.33.2"
dependencies = [
"ab_glyph",
"ahash",
@ -1613,7 +1613,7 @@ dependencies = [
[[package]]
name = "epaint_default_fonts"
version = "0.33.0"
version = "0.33.2"
[[package]]
name = "equivalent"
@ -3425,7 +3425,7 @@ checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
[[package]]
name = "popups"
version = "0.33.0"
version = "0.33.2"
dependencies = [
"eframe",
"env_logger",
@ -4332,6 +4332,15 @@ dependencies = [
"env_logger",
]
[[package]]
name = "text_input_test"
version = "0.1.0"
dependencies = [
"eframe",
"egui_extras",
"env_logger",
]
[[package]]
name = "thiserror"
version = "1.0.66"
@ -5748,7 +5757,7 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
[[package]]
name = "xtask"
version = "0.33.0"
version = "0.33.2"
[[package]]
name = "yaml-rust"

View File

@ -24,7 +24,7 @@ members = [
edition = "2024"
license = "MIT OR Apache-2.0"
rust-version = "1.88"
version = "0.33.0"
version = "0.33.2"
[profile.release]
@ -55,18 +55,18 @@ opt-level = 2
[workspace.dependencies]
emath = { version = "0.33.0", path = "crates/emath", default-features = false }
ecolor = { version = "0.33.0", path = "crates/ecolor", default-features = false }
epaint = { version = "0.33.0", path = "crates/epaint", default-features = false }
epaint_default_fonts = { version = "0.33.0", path = "crates/epaint_default_fonts" }
egui = { version = "0.33.0", path = "crates/egui", default-features = false }
egui-winit = { version = "0.33.0", path = "crates/egui-winit", default-features = false }
egui_extras = { version = "0.33.0", path = "crates/egui_extras", default-features = false }
egui-wgpu = { version = "0.33.0", path = "crates/egui-wgpu", default-features = false }
egui_demo_lib = { version = "0.33.0", path = "crates/egui_demo_lib", default-features = false }
egui_glow = { version = "0.33.0", path = "crates/egui_glow", default-features = false }
egui_kittest = { version = "0.33.1", path = "crates/egui_kittest", default-features = false }
eframe = { version = "0.33.0", path = "crates/eframe", default-features = false }
emath = { version = "0.33.2", path = "crates/emath", default-features = false }
ecolor = { version = "0.33.2", path = "crates/ecolor", default-features = false }
epaint = { version = "0.33.2", path = "crates/epaint", default-features = false }
epaint_default_fonts = { version = "0.33.2", path = "crates/epaint_default_fonts" }
egui = { version = "0.33.2", path = "crates/egui", default-features = false }
egui-winit = { version = "0.33.2", path = "crates/egui-winit", default-features = false }
egui_extras = { version = "0.33.2", path = "crates/egui_extras", default-features = false }
egui-wgpu = { version = "0.33.2", path = "crates/egui-wgpu", default-features = false }
egui_demo_lib = { version = "0.33.2", path = "crates/egui_demo_lib", default-features = false }
egui_glow = { version = "0.33.2", path = "crates/egui_glow", default-features = false }
egui_kittest = { version = "0.33.2", path = "crates/egui_kittest", default-features = false }
eframe = { version = "0.33.2", path = "crates/eframe", default-features = false }
accesskit = "0.21.1"
accesskit_consumer = "0.30.1"

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.33.2 - 2025-11-13
Nothing new
## 0.33.0 - 2025-10-09
* Align `Color32` to 4 bytes [#7318](https://github.com/emilk/egui/pull/7318) by [@anti-social](https://github.com/anti-social)
* Make the `hex_color` macro `const` [#7444](https://github.com/emilk/egui/pull/7444) by [@YgorSouza](https://github.com/YgorSouza)

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.33.2 - 2025-11-13
* Fix jittering during window resize on MacOS for WGPU/Metal [#7641](https://github.com/emilk/egui/pull/7641) by [@aspcartman](https://github.com/aspcartman)
* Make sure `native_pixels_per_point` is set during app creation [#7683](https://github.com/emilk/egui/pull/7683) by [@emilk](https://github.com/emilk)
## 0.33.0 - 2025-10-09
### ⭐ Added
* Add an option to limit the repaint rate in the web runner [#7482](https://github.com/emilk/egui/pull/7482) by [@s-nie](https://github.com/s-nie)

View File

@ -1041,11 +1041,23 @@ impl GlutinWindowContext {
let mut viewport_from_window = HashMap::default();
let mut window_from_viewport = OrderedViewportIdMap::default();
let mut info = ViewportInfo::default();
let mut viewport_info = ViewportInfo::default();
if let Some(window) = &window {
viewport_from_window.insert(window.id(), ViewportId::ROOT);
window_from_viewport.insert(ViewportId::ROOT, window.id());
egui_winit::update_viewport_info(&mut info, egui_ctx, window, true);
egui_winit::update_viewport_info(&mut viewport_info, egui_ctx, window, true);
// Tell egui right away about native_pixels_per_point etc,
// so that the app knows about it during app creation:
let pixels_per_point = egui_winit::pixels_per_point(egui_ctx, window);
egui_ctx.input_mut(|i| {
i.raw
.viewports
.insert(ViewportId::ROOT, viewport_info.clone());
i.pixels_per_point = pixels_per_point;
});
}
let mut viewports = OrderedViewportIdMap::default();
@ -1056,7 +1068,7 @@ impl GlutinWindowContext {
class: ViewportClass::Root,
builder: viewport_builder,
deferred_commands: vec![],
info,
info: viewport_info,
actions_requested: Default::default(),
viewport_ui_cb: None,
gl_surface: None,

View File

@ -199,6 +199,22 @@ impl<'app> WgpuWinitApp<'app> {
},
));
let mut viewport_info = ViewportInfo::default();
egui_winit::update_viewport_info(&mut viewport_info, &egui_ctx, &window, true);
{
// Tell egui right away about native_pixels_per_point etc,
// so that the app knows about it during app creation:
let pixels_per_point = egui_winit::pixels_per_point(&egui_ctx, &window);
egui_ctx.input_mut(|i| {
i.raw
.viewports
.insert(ViewportId::ROOT, viewport_info.clone());
i.pixels_per_point = pixels_per_point;
});
}
let window = Arc::new(window);
{
@ -278,9 +294,6 @@ impl<'app> WgpuWinitApp<'app> {
let mut viewport_from_window = HashMap::default();
viewport_from_window.insert(window.id(), ViewportId::ROOT);
let mut info = ViewportInfo::default();
egui_winit::update_viewport_info(&mut info, &egui_ctx, &window, true);
let mut viewports = Viewports::default();
viewports.insert(
ViewportId::ROOT,
@ -289,7 +302,7 @@ impl<'app> WgpuWinitApp<'app> {
class: ViewportClass::Root,
builder,
deferred_commands: vec![],
info,
info: viewport_info,
actions_requested: Default::default(),
viewport_ui_cb: None,
window: Some(window),

View File

@ -65,6 +65,14 @@ impl AppRunner {
o.zoom_factor = 1.0;
});
// Tell egui right away about native_pixels_per_point
// so that the app knows about it during app creation:
egui_ctx.input_mut(|i| {
let viewport_info = i.raw.viewports.entry(egui::ViewportId::ROOT).or_default();
viewport_info.native_pixels_per_point = Some(super::native_pixels_per_point());
i.pixels_per_point = super::native_pixels_per_point();
});
let cc = epi::CreationContext {
egui_ctx: egui_ctx.clone(),
integration_info: info.clone(),

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.33.2 - 2025-11-13
* Fix jittering during window resize on MacOS for WGPU/Metal [#7641](https://github.com/emilk/egui/pull/7641) by [@aspcartman](https://github.com/aspcartman)
## 0.33.0 - 2025-10-09
### 🔧 Changed
* Update wgpu to 26 and wasm-bindgen to 0.2.100 [#7540](https://github.com/emilk/egui/pull/7540) by [@Kumpelinus](https://github.com/Kumpelinus)

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.33.2 - 2025-11-13
* Don't enable `arboard` on iOS [#7663](https://github.com/emilk/egui/pull/7663) by [@irh](https://github.com/irh)
## 0.33.0 - 2025-10-09
### ⭐ Added
* Add rotation gesture support for trackpad sources [#7453](https://github.com/emilk/egui/pull/7453) by [@thatcomputerguy0101](https://github.com/thatcomputerguy0101)

View File

@ -106,6 +106,14 @@ pub struct State {
allow_ime: bool,
ime_rect_px: Option<egui::Rect>,
/// When true, IME events are completely ignored and text input relies solely
/// on keyboard events. This is a workaround for systems where IME causes issues,
/// such as IBus on Wayland where each character triggers an IME commit that
/// disrupts normal typing flow.
///
/// See <https://github.com/emilk/egui/issues/7485>
ime_disabled: bool,
}
impl State {
@ -148,6 +156,8 @@ impl State {
allow_ime: false,
ime_rect_px: None,
ime_disabled: should_disable_ime_for_buggy_systems(),
};
slf.egui_input
@ -205,6 +215,30 @@ impl State {
self.allow_ime = allow;
}
/// Returns whether IME is disabled as a workaround for buggy systems.
///
/// When IME is disabled, text input relies solely on keyboard events.
/// This is automatically enabled on systems where IME causes issues,
/// such as IBus on Wayland.
///
/// See <https://github.com/emilk/egui/issues/7485>
pub fn ime_disabled(&self) -> bool {
self.ime_disabled
}
/// Explicitly enable or disable the IME workaround.
///
/// When set to `true`, IME events are ignored and text input relies
/// solely on keyboard events. This is useful for systems where IME
/// causes issues, such as IBus on Wayland.
///
/// By default, this is automatically detected based on the environment.
///
/// See <https://github.com/emilk/egui/issues/7485>
pub fn set_ime_disabled(&mut self, disabled: bool) {
self.ime_disabled = disabled;
}
#[inline]
pub fn egui_ctx(&self) -> &egui::Context {
&self.egui_ctx
@ -350,6 +384,31 @@ impl State {
}
WindowEvent::Ime(ime) => {
// When IME is disabled as a workaround for buggy systems (like IBus on Wayland),
// we convert IME Commit events to regular Text events instead of using IME protocol.
// This works around the bug where IBus on Wayland sends each character as an
// IME commit which disrupts normal text editing flow.
// See https://github.com/emilk/egui/issues/7485
if self.ime_disabled {
// Only handle Commit events - convert them to regular Text events
if let winit::event::Ime::Commit(text) = ime {
if !text.is_empty() && text != "\n" && text != "\r" {
self.egui_input
.events
.push(egui::Event::Text(text.clone()));
return EventResponse {
repaint: true,
consumed: self.egui_ctx.wants_keyboard_input(),
};
}
}
// Ignore all other IME events (Enabled, Disabled, Preedit)
return EventResponse {
repaint: false,
consumed: false,
};
}
// on Mac even Cmd-C is pressed during ime, a `c` is pushed to Preedit.
// So no need to check is_mac_cmd.
//
@ -907,7 +966,10 @@ impl State {
self.set_cursor_icon(window, cursor_icon);
let allow_ime = ime.is_some();
// When IME is disabled as a workaround for buggy systems, don't enable IME at all.
// This ensures the system doesn't try to intercept keyboard input.
// See https://github.com/emilk/egui/issues/7485
let allow_ime = ime.is_some() && !self.ime_disabled;
if self.allow_ime != allow_ime {
self.allow_ime = allow_ime;
profiling::scope!("set_ime_allowed");
@ -915,6 +977,10 @@ impl State {
}
if let Some(ime) = ime {
// Skip IME cursor positioning if IME is disabled
if self.ime_disabled {
self.ime_rect_px = None;
} else {
let pixels_per_point = pixels_per_point(&self.egui_ctx, window);
let ime_rect_px = pixels_per_point * ime.rect;
if self.ime_rect_px != Some(ime_rect_px)
@ -933,6 +999,7 @@ impl State {
},
);
}
}
} else {
self.ime_rect_px = None;
}
@ -1073,6 +1140,71 @@ fn open_url_in_browser(_url: &str) {
}
}
/// Detects if we're running on a system where IME is known to cause issues.
///
/// Currently detects IBus on Wayland, which has a bug where each character
/// triggers an IME commit that disrupts normal typing flow.
///
/// The detection checks:
/// 1. We're on Linux
/// 2. We're running under Wayland (XDG_SESSION_TYPE=wayland or WAYLAND_DISPLAY is set)
/// 3. IBus is the active input method (GTK_IM_MODULE=ibus or IBUS_* env vars are set)
///
/// Users can override this by setting the environment variable:
/// - `EGUI_IME_DISABLED=1` to force IME disabled (use keyboard events only)
/// - `EGUI_IME_DISABLED=0` to force IME enabled (normal behavior)
///
/// See <https://github.com/emilk/egui/issues/7485>
fn should_disable_ime_for_buggy_systems() -> bool {
// Allow explicit override via environment variable
if let Ok(val) = std::env::var("EGUI_IME_DISABLED") {
return val == "1" || val.eq_ignore_ascii_case("true");
}
// Only applies to Linux
if !cfg!(target_os = "linux") {
return false;
}
// Check if we're on Wayland
let is_wayland = std::env::var("XDG_SESSION_TYPE")
.map(|v| v == "wayland")
.unwrap_or(false)
|| std::env::var("WAYLAND_DISPLAY").is_ok();
if !is_wayland {
return false;
}
// Check if IBus is the input method
// IBus can be detected via several environment variables:
// - GTK_IM_MODULE=ibus (GTK apps)
// - QT_IM_MODULE=ibus (Qt apps)
// - XMODIFIERS=@im=ibus (X11 input method)
// - IBUS_DAEMON_PID (set when ibus-daemon is running)
let is_ibus = std::env::var("GTK_IM_MODULE")
.map(|v| v == "ibus")
.unwrap_or(false)
|| std::env::var("QT_IM_MODULE")
.map(|v| v == "ibus")
.unwrap_or(false)
|| std::env::var("XMODIFIERS")
.map(|v| v.contains("ibus"))
.unwrap_or(false)
|| std::env::var("IBUS_DAEMON_PID").is_ok();
if is_ibus {
log::warn!(
"Detected IBus on Wayland - disabling IME to work around text input bug. \
Set EGUI_IME_DISABLED=0 to override. \
See https://github.com/emilk/egui/issues/7485"
);
return true;
}
false
}
/// Winit sends special keys (backspace, delete, F1, …) as characters.
/// Ignore those.
/// We also ignore '\r', '\n', '\t'.

View File

@ -525,11 +525,21 @@ impl Area {
true,
);
// Used to prevent drift
let pivot_at_start_of_drag_id = id.with("pivot_at_drag_start");
if movable
&& move_response.dragged()
&& let Some(pivot_pos) = &mut state.pivot_pos
{
*pivot_pos += move_response.drag_delta();
let pivot_at_start_of_drag = ctx.data_mut(|data| {
*data.get_temp_mut_or::<Pos2>(pivot_at_start_of_drag_id, *pivot_pos)
});
*pivot_pos =
pivot_at_start_of_drag + move_response.total_drag_delta().unwrap_or_default();
} else {
ctx.data_mut(|data| data.remove::<Pos2>(pivot_at_start_of_drag_id));
}
if (move_response.dragged() || move_response.clicked())
@ -543,13 +553,14 @@ impl Area {
move_response
};
state.set_left_top_pos(round_area_position(
ctx,
if constrain {
state.set_left_top_pos(
Context::constrain_window_rect_to_area(state.rect(), constrain_rect).min,
);
}
state.set_left_top_pos(state.left_top_pos());
Context::constrain_window_rect_to_area(state.rect(), constrain_rect).min
} else {
state.left_top_pos()
},
));
// Update response with possibly moved/constrained rect:
move_response.rect = state.rect();
@ -570,6 +581,16 @@ impl Area {
}
}
fn round_area_position(ctx: &Context, pos: Pos2) -> Pos2 {
// We round a lot of rendering to pixels, so we round the whole
// area positions to pixels too, so avoid widgets appearing to float
// around independently of each other when the area is dragged.
// But just in case pixels_per_point is irrational,
// we then also round to ui coordinates:
pos.round_to_pixels(ctx.pixels_per_point()).round_ui()
}
impl Prepared {
pub(crate) fn state(&self) -> &AreaState {
&self.state

View File

@ -239,7 +239,7 @@ impl ComboBox {
let mut ir = combo_box_dyn(
ui,
button_id,
selected_text,
selected_text.clone(),
menu_contents,
icon,
wrap_mode,
@ -247,14 +247,16 @@ impl ComboBox {
popup_style,
(width, height),
);
if let Some(label) = label {
ir.response.widget_info(|| {
WidgetInfo::labeled(WidgetType::ComboBox, ui.is_enabled(), label.text())
let mut info = WidgetInfo::new(WidgetType::ComboBox);
info.enabled = ui.is_enabled();
info.current_text_value = Some(selected_text.text().to_owned());
info
});
ir.response |= ui.label(label);
} else {
ir.response
.widget_info(|| WidgetInfo::labeled(WidgetType::ComboBox, ui.is_enabled(), ""));
if let Some(label) = label {
let label_response = ui.label(label);
ir.response = ir.response.labelled_by(label_response.id);
ir.response |= label_response;
}
ir
})

View File

@ -11,7 +11,7 @@ use crate::{
///
/// You can show multiple modals on top of each other. The topmost modal will always be
/// the most recently shown one.
/// If multiple modals are newly shown in the same frame, the order of the modals not undefined
/// If multiple modals are newly shown in the same frame, the order of the modals is undefined
/// (either first or second could be top).
pub struct Modal {
pub area: Area,

View File

@ -2,9 +2,11 @@
use std::ops::{Add, AddAssign, BitOr, BitOrAssign};
use emath::GuiRounding as _;
use crate::{
Context, CursorIcon, Id, NumExt as _, Pos2, Rangef, Rect, Sense, Ui, UiBuilder, UiKind,
UiStackInfo, Vec2, Vec2b, emath, epaint, lerp, pass_state, pos2, remap, remap_clamp,
Context, CursorIcon, Id, NumExt as _, Pos2, Rangef, Rect, Response, Sense, Ui, UiBuilder,
UiKind, UiStackInfo, Vec2, Vec2b, emath, epaint, lerp, pass_state, pos2, remap, remap_clamp,
};
#[derive(Clone, Copy, Debug)]
@ -659,6 +661,9 @@ struct Prepared {
/// not for us to handle so we save it and restore it after this [`ScrollArea`] is done.
saved_scroll_target: [Option<pass_state::ScrollTarget>; 2],
/// The response from dragging the background (if enabled)
background_drag_response: Option<Response>,
animated: bool,
}
@ -745,6 +750,12 @@ impl ScrollArea {
}
let content_max_rect = Rect::from_min_size(inner_rect.min - state.offset, content_max_size);
// Round to pixels to avoid widgets appearing to "float" when scrolling fractional amounts:
let content_max_rect = content_max_rect
.round_to_pixels(ui.pixels_per_point())
.round_ui();
let mut content_ui = ui.new_child(
UiBuilder::new()
.ui_stack_info(UiStackInfo::new(UiKind::ScrollArea))
@ -772,10 +783,8 @@ impl ScrollArea {
let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size);
let dt = ui.input(|i| i.stable_dt).at_most(0.1);
if scroll_source.drag
&& ui.is_enabled()
&& (state.content_is_too_large[0] || state.content_is_too_large[1])
{
let background_drag_response =
if scroll_source.drag && ui.is_enabled() && state.content_is_too_large.any() {
// Drag contents to scroll (for touch screens mostly).
// We must do this BEFORE adding content to the `ScrollArea`,
// or we will steal input from the widgets we contain.
@ -802,8 +811,8 @@ impl ScrollArea {
.as_ref()
.is_some_and(|response| response.drag_stopped())
{
state.vel =
direction_enabled.to_vec2() * ui.input(|input| input.pointer.velocity());
state.vel = direction_enabled.to_vec2()
* ui.input(|input| input.pointer.velocity());
}
for d in 0..2 {
// Kinetic scrolling
@ -824,19 +833,23 @@ impl ScrollArea {
}
// Set the desired mouse cursors.
if let Some(response) = content_response_option {
if response.dragged() {
if let Some(cursor) = on_drag_cursor {
response.on_hover_cursor(cursor);
}
if let Some(response) = &content_response_option {
if response.dragged()
&& let Some(cursor) = on_drag_cursor
{
ui.ctx().set_cursor_icon(cursor);
} else if response.hovered()
&& let Some(cursor) = on_hover_cursor
{
response.on_hover_cursor(cursor);
}
ui.ctx().set_cursor_icon(cursor);
}
}
content_response_option
} else {
None
};
// Scroll with an animation if we have a target offset (that hasn't been cleared by the code
// above).
for d in 0..2 {
@ -888,6 +901,7 @@ impl ScrollArea {
wheel_scroll_multiplier,
stick_to_end,
saved_scroll_target,
background_drag_response,
animated,
}
}
@ -1003,6 +1017,7 @@ impl Prepared {
wheel_scroll_multiplier,
stick_to_end,
saved_scroll_target,
background_drag_response,
animated,
} = self;
@ -1118,7 +1133,16 @@ impl Prepared {
);
let max_offset = content_size - inner_rect.size();
let is_hovering_outer_rect = ui.rect_contains_pointer(outer_rect);
// Drag-to-scroll?
let is_dragging_background = background_drag_response
.as_ref()
.is_some_and(|r| r.dragged());
let is_hovering_outer_rect = ui.rect_contains_pointer(outer_rect)
&& ui.ctx().dragged_id().is_none()
|| is_dragging_background;
if scroll_source.mouse_wheel && ui.is_enabled() && is_hovering_outer_rect {
let always_scroll_enabled_direction = ui.style().always_scroll_the_only_direction
&& direction_enabled[0] != direction_enabled[1];
@ -1204,6 +1228,7 @@ impl Prepared {
let is_hovering_bar_area = is_hovering_outer_rect
&& ui.rect_contains_pointer(max_bar_rect)
&& !is_dragging_background
|| state.scroll_bar_interaction[d];
let is_hovering_bar_area_t = ui

View File

@ -825,7 +825,7 @@ fn resize_response(
area: &mut area::Prepared,
resize_id: Id,
) {
let Some(mut new_rect) = move_and_resize_window(ctx, &resize_interaction) else {
let Some(mut new_rect) = move_and_resize_window(ctx, resize_id, &resize_interaction) else {
return;
};
@ -847,27 +847,38 @@ fn resize_response(
}
/// Acts on outer rect (outside the stroke)
fn move_and_resize_window(ctx: &Context, interaction: &ResizeInteraction) -> Option<Rect> {
fn move_and_resize_window(ctx: &Context, id: Id, interaction: &ResizeInteraction) -> Option<Rect> {
// Used to prevent drift
let rect_at_start_of_drag_id = id.with("window_rect_at_drag_start");
if !interaction.any_dragged() {
ctx.data_mut(|data| {
data.remove::<Rect>(rect_at_start_of_drag_id);
});
return None;
}
let pointer_pos = ctx.input(|i| i.pointer.interact_pos())?;
let mut rect = interaction.outer_rect; // prevent drift
let total_drag_delta = ctx.input(|i| i.pointer.total_drag_delta())?;
let rect_at_start_of_drag = ctx.data_mut(|data| {
*data.get_temp_mut_or::<Rect>(rect_at_start_of_drag_id, interaction.outer_rect)
});
let mut rect = rect_at_start_of_drag; // prevent drift
// Put the rect in the center of the stroke:
rect = rect.shrink(interaction.window_frame.stroke.width / 2.0);
if interaction.left.drag {
rect.min.x = pointer_pos.x;
rect.min.x += total_drag_delta.x;
} else if interaction.right.drag {
rect.max.x = pointer_pos.x;
rect.max.x += total_drag_delta.x;
}
if interaction.top.drag {
rect.min.y = pointer_pos.y;
rect.min.y += total_drag_delta.y;
} else if interaction.bottom.drag {
rect.max.y = pointer_pos.y;
rect.max.y += total_drag_delta.y;
}
// Return to having the rect outside the stroke:

View File

@ -1336,6 +1336,11 @@ impl PointerState {
self.press_origin
}
/// How far has the pointer moved since the start of the drag (if any)?
pub fn total_drag_delta(&self) -> Option<Vec2> {
Some(self.latest_pos? - self.press_origin?)
}
/// When did the current click/drag originate?
/// `None` if no mouse button is down.
#[inline(always)]

View File

@ -396,7 +396,7 @@ impl Response {
self.drag_stopped() && self.ctx.input(|i| i.pointer.button_released(button))
}
/// If dragged, how many points were we dragged and in what direction?
/// If dragged, how many points were we dragged in since last frame?
#[inline]
pub fn drag_delta(&self) -> Vec2 {
if self.dragged() {
@ -410,7 +410,22 @@ impl Response {
}
}
/// If dragged, how far did the mouse move?
/// If dragged, how many points have we been dragged since the start of the drag?
#[inline]
pub fn total_drag_delta(&self) -> Option<Vec2> {
if self.dragged() {
let mut delta = self.ctx.input(|i| i.pointer.total_drag_delta())?;
if let Some(from_global) = self.ctx.layer_transform_from_global(self.layer_id) {
delta *= from_global.scaling;
}
Some(delta)
} else {
None
}
}
/// If dragged, how far did the mouse move since last frame?
///
/// This will use raw mouse movement if provided by the integration, otherwise will fall back to [`Response::drag_delta`]
/// Raw mouse movement is unaccelerated and unclamped by screen boundaries, and does not relate to any position on the screen.
/// This may be useful in certain situations such as draggable values and 3D cameras, where screen position does not matter.
@ -709,10 +724,9 @@ impl Response {
/// ```
#[must_use]
pub fn interact(&self, sense: Sense) -> Self {
if (self.sense | sense) == self.sense {
// Early-out: we already sense everything we need to sense.
return self.clone();
}
// We could check here if the new Sense equals the old one to avoid the extra create_widget
// call. But that would break calling `interact` on a response from `Context::read_response`
// or `Ui::response`. (See https://github.com/emilk/egui/pull/7713 for more details.)
self.ctx.create_widget(
WidgetRect {

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dc9c22567b76193a7f6753c4217adb3c92afa921c488ba1cf2e14b403814e7ac
size 99841
oid sha256:128ca4e741995ffcdc07b027407d63911ded6c94fe3fe1dd0efecbf9408fb3af
size 99871

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:aff927596be5db77349ec0bbdcc852a0b1467e94c2a553a740a383ae318bad18
oid sha256:bb3f7b5f790830b46d1410c2bbb5e19c6beb403f8fe979eb8d250fba4f89be3e
size 51670

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9d6f055247034fa13ab55c9ec1fca275e6c23999c9a7e01c87af1fcc930faac6
size 66777
oid sha256:170cee9d72a4ab59aa2faf1b77aff4a9eee64f3380aa3f1b256340d88b1dabc2
size 66525

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c91f592571ba654d0a96791662ae7530a1db4c1630b57c795d1c006ea6e46f19
size 256975
oid sha256:f7a7d0e2618b852b5966073438c95cb62901d5410c1473639920b0b0bf2ec59b
size 256913

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4fbcca2b13c94769a62b44853b19f7e841bbb60c9197b3d0bf6e83ef9f8f76d1
size 77815
oid sha256:72f4c6fe4f5ec243506152027e1150f3069caf98511ceef92b8fea4f6a1563d5
size 77614

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a31f0c12bb70449136443f9086103bd5b46356eedc2bb93ae1b6b10684ab69ca
size 36285
oid sha256:611a2d6c793a85eebe807b2ddd4446cc0bc21e4284343dd756e64f0232fb6815
size 35991

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.33.2 - 2025-11-13
Nothing new
## 0.33.0 - 2025-10-09
* Fix: use unique id for resize columns in `Table` [#7414](https://github.com/emilk/egui/pull/7414) by [@zezic](https://github.com/zezic)
* Feat: Add serde serialization to SyntectSettings [#7506](https://github.com/emilk/egui/pull/7506) by [@bircni](https://github.com/bircni)

View File

@ -6,6 +6,10 @@ Changes since the last release can be found at <https://github.com/emilk/egui/co
## 0.33.2 - 2025-11-13
Nothing new
## 0.33.0 - 2025-10-09
* Update MSRV from 1.86 to 1.88 [#7579](https://github.com/emilk/egui/pull/7579) by [@Wumpf](https://github.com/Wumpf)

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.33.2 - 2025-11-13
Nothing new
## 0.33.1 - 2025-10-15
* Add `egui_kittest::HarnessBuilder::with_options` [#7638](https://github.com/emilk/egui/pull/7638) by [@emilk](https://github.com/emilk)

View File

@ -1,6 +1,6 @@
[package]
name = "egui_kittest"
version = "0.33.1"
version.workspace = true
authors = ["Lucas Meurer <lucasmeurer96@gmail.com>", "Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "Testing library for egui based on kittest and AccessKit"
edition.workspace = true

View File

@ -5,6 +5,11 @@ All notable changes to the `emath` crate will be noted in this file.
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.33.2 - 2025-11-13
* Fix edge cases in "smart aiming" in sliders [#7680](https://github.com/emilk/egui/pull/7680) by [@emilk](https://github.com/emilk)
## 0.33.0 - 2025-10-09
* Add `emath::fast_midpoint` [#7435](https://github.com/emilk/egui/pull/7435) by [@emilk](https://github.com/emilk)
* Generate changelogs for emath [#7513](https://github.com/emilk/egui/pull/7513) 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.33.2 - 2025-11-13
Nothing new
## 0.33.0 - 2025-10-09
* Remove the `deadlock_detection` feature [#7497](https://github.com/emilk/egui/pull/7497) by [@lucasmerlin](https://github.com/lucasmerlin)
* More even text kerning [#7431](https://github.com/emilk/egui/pull/7431) by [@valadaptive](https://github.com/valadaptive)

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.33.2 - 2025-11-13
Nothing new
## 0.33.0 - 2025-10-09
Nothing new

View File

@ -0,0 +1,14 @@
[package]
name = "text_input_test"
version = "0.1.0"
edition = "2024"
rust-version = "1.88"
publish = false
[lints]
workspace = true
[dependencies]
eframe = { workspace = true, features = ["default"] }
egui_extras = { workspace = true, features = ["default", "syntect"] }
env_logger = { workspace = true, features = ["auto-color", "humantime"] }

View File

@ -0,0 +1,205 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
#![allow(rustdoc::missing_crate_level_docs)]
use eframe::egui;
fn main() -> eframe::Result {
env_logger::init();
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default().with_inner_size([800.0, 600.0]),
..Default::default()
};
eframe::run_native(
"Text Input Test - IBus/Wayland",
options,
Box::new(|_cc| Ok(Box::<TestApp>::default())),
)
}
struct TestApp {
// Single-line text inputs
single_line_1: String,
single_line_2: String,
// Multi-line text input
multi_line: String,
// Code editor content
code: String,
// Password field
password: String,
// Input event log
event_log: Vec<String>,
}
impl Default for TestApp {
fn default() -> Self {
Self {
single_line_1: String::new(),
single_line_2: "Pre-filled text".to_owned(),
multi_line: "Type multiple lines here.\nLine 2\nLine 3".to_owned(),
code: r#"fn main() {
println!("Hello, world!");
}
"#.to_owned(),
password: String::new(),
event_log: Vec::new(),
}
}
}
impl eframe::App for TestApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
// Log input events
ctx.input(|i| {
for event in &i.events {
match event {
egui::Event::Text(text) => {
self.event_log.push(format!(">>> TEXT: {:?}", text));
}
egui::Event::Key { key, pressed, modifiers, .. } => {
self.event_log.push(format!("KEY: {:?} pressed={} (mods: {:?})", key, pressed, modifiers));
}
egui::Event::Ime(ime) => {
// Filter out the spammy Disabled events
match ime {
egui::ImeEvent::Disabled => {} // Skip logging these
_ => self.event_log.push(format!("IME: {:?}", ime)),
}
}
egui::Event::Paste(text) => {
self.event_log.push(format!("PASTE: {:?}", text));
}
_ => {}
}
}
});
// Keep event log manageable
if self.event_log.len() > 100 {
self.event_log.drain(0..50);
}
egui::SidePanel::right("event_log").show(ctx, |ui| {
ui.heading("Input Events");
if ui.button("Clear").clicked() {
self.event_log.clear();
}
ui.separator();
egui::ScrollArea::vertical().show(ui, |ui| {
for event in self.event_log.iter().rev() {
ui.label(event);
}
});
});
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("Text Input Test - IBus/Wayland Bug");
ui.label("Test various text input widgets. If IBus under Wayland is broken, typing won't work.");
// Show environment info
ui.add_space(5.0);
ui.group(|ui| {
ui.label("Environment Detection:");
let session_type = std::env::var("XDG_SESSION_TYPE").unwrap_or_else(|_| "unknown".to_string());
let wayland_display = std::env::var("WAYLAND_DISPLAY").is_ok();
let gtk_im = std::env::var("GTK_IM_MODULE").unwrap_or_else(|_| "not set".to_string());
let qt_im = std::env::var("QT_IM_MODULE").unwrap_or_else(|_| "not set".to_string());
let xmodifiers = std::env::var("XMODIFIERS").unwrap_or_else(|_| "not set".to_string());
let ime_override = std::env::var("EGUI_IME_DISABLED").unwrap_or_else(|_| "not set".to_string());
ui.label(format!(" XDG_SESSION_TYPE: {session_type}"));
ui.label(format!(" WAYLAND_DISPLAY set: {wayland_display}"));
ui.label(format!(" GTK_IM_MODULE: {gtk_im}"));
ui.label(format!(" QT_IM_MODULE: {qt_im}"));
ui.label(format!(" XMODIFIERS: {xmodifiers}"));
ui.label(format!(" EGUI_IME_DISABLED: {ime_override}"));
let is_wayland = session_type == "wayland" || wayland_display;
let is_ibus = gtk_im == "ibus" || qt_im == "ibus" || xmodifiers.contains("ibus");
if is_wayland && is_ibus {
ui.colored_label(egui::Color32::GREEN,
"IBus on Wayland detected - IME workaround is active!");
}
});
ui.separator();
egui::ScrollArea::vertical().show(ui, |ui| {
ui.group(|ui| {
ui.heading("Single-line Text Edits");
ui.horizontal(|ui| {
ui.label("Empty field:");
ui.text_edit_singleline(&mut self.single_line_1);
});
ui.horizontal(|ui| {
ui.label("Pre-filled:");
ui.text_edit_singleline(&mut self.single_line_2);
});
});
ui.add_space(10.0);
ui.group(|ui| {
ui.heading("Password Field");
ui.horizontal(|ui| {
ui.label("Password:");
ui.add(egui::TextEdit::singleline(&mut self.password).password(true));
});
});
ui.add_space(10.0);
ui.group(|ui| {
ui.heading("Multi-line Text Edit");
ui.add(
egui::TextEdit::multiline(&mut self.multi_line)
.desired_width(f32::INFINITY)
.desired_rows(6)
);
});
ui.add_space(10.0);
ui.group(|ui| {
ui.heading("Code Editor (with syntax highlighting)");
let theme = egui_extras::syntax_highlighting::CodeTheme::from_memory(ui.ctx(), ui.style());
let mut layouter = |ui: &egui::Ui, string: &dyn egui::TextBuffer, wrap_width: f32| {
let mut layout_job = egui_extras::syntax_highlighting::highlight(
ui.ctx(),
ui.style(),
&theme,
string.as_str(),
"rs",
);
layout_job.wrap.max_width = wrap_width;
ui.fonts_mut(|f| f.layout_job(layout_job))
};
ui.add(
egui::TextEdit::multiline(&mut self.code)
.font(egui::TextStyle::Monospace)
.code_editor()
.desired_width(f32::INFINITY)
.desired_rows(10)
.layouter(&mut layouter)
);
});
ui.add_space(10.0);
ui.group(|ui| {
ui.heading("Current Values");
ui.label(format!("Single line 1: {:?}", self.single_line_1));
ui.label(format!("Single line 2: {:?}", self.single_line_2));
ui.label(format!("Password length: {}", self.password.len()));
ui.label(format!("Multi-line lines: {}", self.multi_line.lines().count()));
ui.label(format!("Code lines: {}", self.code.lines().count()));
});
});
});
}
}

View File

@ -1,5 +1,5 @@
use egui::accesskit::Role;
use egui::{Align, Color32, Image, Label, Layout, RichText, TextWrapMode, include_image};
use egui::{Align, Color32, Image, Label, Layout, RichText, Sense, TextWrapMode, include_image};
use egui_kittest::Harness;
use egui_kittest::kittest::Queryable as _;
@ -61,3 +61,60 @@ fn text_edit_rtl() {
harness.snapshot(format!("text_edit_rtl_{i}"));
}
}
#[test]
fn combobox_should_have_value() {
let harness = Harness::new_ui(|ui| {
egui::ComboBox::from_label("Select an option")
.selected_text("Option 1")
.show_ui(ui, |_ui| {});
});
assert_eq!(
harness.get_by_label("Select an option").value().as_deref(),
Some("Option 1")
);
}
/// This test ensures that `ui.response().interact(...)` works correctly.
///
/// This was broken, because there was an optimization in [`egui::Response::interact`]
/// which caused the [`Sense`] of the original response to flip-flop between `click` and `hover`
/// between frames.
///
/// See <https://github.com/emilk/egui/pull/7713> for more details.
#[test]
fn interact_on_ui_response_should_be_stable() {
let mut first_frame = true;
let mut click_count = 0;
let mut harness = Harness::new_ui(|ui| {
let ui_response = ui.response();
if !first_frame {
assert!(
ui_response.sense.contains(Sense::click()),
"ui.response() didn't have click sense even though we called interact(Sense::click()) last frame"
);
}
// Add a label so we have something to click with kittest
ui.add(
Label::new("senseless label")
.sense(Sense::hover())
.selectable(false),
);
let click_response = ui_response.interact(Sense::click());
if click_response.clicked() {
click_count += 1;
}
first_frame = false;
});
for i in 0..=10 {
harness.run_steps(i);
harness.get_by_label("senseless label").click();
}
drop(harness);
assert_eq!(click_count, 10, "We missed some clicks!");
}