1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-29 04:40:03 -04:00

Merge branch 'main' into theme_plugin

This commit is contained in:
Emil Ernerfeldt
2026-07-28 05:21:56 -07:00
committed by GitHub
82 changed files with 1067 additions and 898 deletions

View File

@@ -38,7 +38,7 @@ jobs:
with:
profile: minimal
target: wasm32-unknown-unknown
toolchain: 1.92.0
toolchain: 1.95.0
override: true
- uses: Swatinem/rust-cache@v2

View File

@@ -20,7 +20,7 @@ jobs:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.92.0
toolchain: 1.95.0
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
with:

View File

@@ -5,7 +5,7 @@ name: Rust
env:
RUSTFLAGS: -D warnings
RUSTDOCFLAGS: -D warnings
NIGHTLY_VERSION: nightly-2025-09-16
NIGHTLY_VERSION: nightly-2026-04-02
jobs:
fmt-crank-check-test:
@@ -19,7 +19,7 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.92.0
toolchain: 1.95.0
- name: Install packages (Linux)
if: runner.os == 'Linux'
@@ -74,7 +74,7 @@ jobs:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.92.0
toolchain: 1.95.0
targets: wasm32-unknown-unknown
- run: sudo apt-get update && sudo apt-get install libgtk-3-dev libatk1.0-dev
@@ -94,7 +94,7 @@ jobs:
- name: wasm-bindgen
uses: jetli/wasm-bindgen-action@v0.1.0
with:
version: "0.2.108" # Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml
version: "0.2.126" # Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml
- run: ./scripts/wasm_bindgen_check.sh --skip-setup
@@ -148,7 +148,7 @@ jobs:
- uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v2
with:
rust-version: "1.92.0"
rust-version: "1.95.0"
log-level: error
command: check
arguments: --target ${{ matrix.target }}
@@ -164,7 +164,7 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.92.0
toolchain: 1.95.0
targets: aarch64-linux-android
- name: Set up cargo cache
@@ -186,7 +186,7 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.92.0
toolchain: 1.95.0
targets: aarch64-apple-ios
- name: Set up cargo cache
@@ -206,7 +206,7 @@ jobs:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.92.0
toolchain: 1.95.0
- name: Set up cargo cache
uses: Swatinem/rust-cache@v2
@@ -228,7 +228,7 @@ jobs:
lfs: true
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.92.0
toolchain: 1.95.0
- name: Set up cargo cache
uses: Swatinem/rust-cache@v2

1123
Cargo.lock

File diff suppressed because it is too large Load Diff

View File

@@ -24,7 +24,7 @@ members = [
[workspace.package]
edition = "2024"
license = "MIT OR Apache-2.0"
rust-version = "1.92"
rust-version = "1.95"
version = "0.35.0"
@@ -71,8 +71,8 @@ egui_kittest = { version = "0.35.0", path = "crates/egui_kittest", default-featu
eframe = { version = "0.35.0", path = "crates/eframe", default-features = false }
accesskit = "0.24.1"
accesskit_consumer = "0.35.0"
accesskit_winit = "0.32.0"
accesskit_consumer = "0.35.0" # Can't update to 0.36+: kittest 0.4 pins accesskit_consumer 0.35, so bumping splits it into two versions
accesskit_winit = "0.32.0" # Can't update to 0.33: it needs accesskit_macos 0.26.2, which pulls accesskit_consumer 0.37, duplicating the 0.35 that kittest 0.4 needs
ahash = { version = "0.8.12", default-features = false, features = [
"no-rng", # we don't need DOS-protection, so we let users opt-in to it instead
"std",
@@ -97,12 +97,12 @@ font-types = { version = "0.11.3", default-features = false, features = [
glow = "0.17.0"
glutin = { version = "0.32.3", default-features = false }
glutin-winit = { version = "0.5.0", default-features = false }
harfrust = "0.7.0" # Can't update to 0.10: it needs read-fonts 0.40/font-types 0.12, but vello_cpu's glifo 0.1.1 pins read-fonts 0.39/font-types 0.11, so bumping duplicates them
harfrust = "0.7.0" # Can't update to 0.8+: newer versions need read-fonts 0.40+/font-types 0.12, but vello_cpu's glifo 0.1.1 pins read-fonts 0.39/font-types 0.11, so bumping duplicates them
home = "0.5.12"
image = { version = "0.25.6", default-features = false } # Can't update to 0.25.7+: it needs png 0.18, which only matches resvg once resvg moves to tiny-skia 0.12 — blocked, see resvg below
itertools = "0.14.0" # 0.15 splits us from puffin 0.20's itertools 0.14, adding a cargo-deny duplicate
itertools = "0.15.0"
jiff = { version = "0.2.29", default-features = false }
js-sys = "0.3.77"
js-sys = "0.3.103"
kittest = { version = "0.4.0" }
log = { version = "0.4.33", features = ["std"] }
memoffset = "0.9.1"
@@ -132,7 +132,7 @@ ron = "0.12.2"
self_cell = "1.2"
serde = { version = "1.0", features = ["derive"] }
serde_bytes = "0.11.19"
similar-asserts = "2.0.0"
similar-asserts = "2.0"
skrifa = { version = "0.42.1", default-features = false, features = [
"std",
"autohint_shaping",
@@ -144,9 +144,9 @@ syntect = { version = "5.3", default-features = false }
tempfile = "3.27"
thiserror = "2.0"
tokio = "1.52"
toml = { version = "1.0", default-features = false } # 1.1+ pulls winnow 1.x, duplicating the winnow 0.7 that winit's toml_edit needs
toml = { version = "1.0", default-features = false }
type-map = "0.5.1"
unicode_names2 = { version = "3.1.0", default-features = false }
unicode_names2 = { version = "3.1", default-features = false }
unicode-general-category = "1.1"
unicode-segmentation = "1.13"
vello_cpu = { version = "0.0.9", default-features = false, features = [
@@ -154,13 +154,13 @@ vello_cpu = { version = "0.0.9", default-features = false, features = [
"u8_pipeline",
"f32_pipeline",
] }
wasm-bindgen = "0.2.108" # Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml. Don't update this spuriously, because of https://github.com/rerun-io/rerun/issues/8766
wasm-bindgen-futures = "0.4.58"
wasm-bindgen = "0.2.126" # Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml. Don't update this spuriously, because of https://github.com/rerun-io/rerun/issues/8766
wasm-bindgen-futures = "0.4.76"
wayland-cursor = { version = "0.31.14", default-features = false }
web-sys = "0.3.77"
web-sys = "0.3.103"
web-time = "1.1" # Timekeeping for native and web
webbrowser = "1.2"
wgpu = { version = "29.0", default-features = false, features = ["std"] }
wgpu = { version = "30.0", default-features = false, features = ["std"] }
windows-sys = "0.61.2"
winit = { version = "0.30.13", default-features = false }
@@ -205,6 +205,7 @@ cloned_instead_of_copied = "warn"
coerce_container_to_any = "warn"
dbg_macro = "warn"
debug_assert_with_mut_call = "warn"
decimal_bitwise_operands = "warn"
default_union_representation = "warn"
derive_partial_eq_without_eq = "warn"
disallowed_macros = "warn" # See clippy.toml
@@ -217,9 +218,10 @@ doc_comment_double_space_linebreaks = "warn"
doc_include_without_cfg = "warn"
doc_link_with_quotes = "warn"
doc_markdown = "warn"
duration_suboptimal_units = "warn"
elidable_lifetime_names = "warn"
empty_enum = "warn"
empty_enum_variants_with_brackets = "warn"
empty_enums = "warn"
empty_line_after_outer_attr = "warn"
enum_glob_use = "warn"
equatable_if_let = "warn"
@@ -270,6 +272,7 @@ lossy_float_literal = "warn"
macro_use_imports = "warn"
manual_assert = "warn"
manual_clamp = "warn"
manual_ilog2 = "warn"
manual_instant_elapsed = "warn"
manual_is_power_of_two = "warn"
manual_is_variant_and = "warn"
@@ -299,10 +302,12 @@ needless_for_each = "warn"
needless_pass_by_ref_mut = "warn"
needless_pass_by_value = "warn"
needless_raw_string_hashes = "warn"
needless_type_cast = "warn"
negative_feature_names = "warn"
non_std_lazy_statics = "warn"
non_zero_suggestions = "warn"
nonstandard_macro_braces = "warn"
only_used_in_recursion = "warn"
option_as_ref_cloned = "warn"
option_option = "warn"
or_fun_call = "warn"
@@ -325,6 +330,7 @@ ref_patterns = "warn"
rest_pat_in_fully_bound_structs = "warn"
return_and_then = "warn"
same_functions_in_if_condition = "warn"
same_length_and_capacity = "warn"
self_only_used_in_recursion = "warn"
semicolon_if_nothing_returned = "warn"
set_contains_or_insert = "warn"
@@ -341,6 +347,7 @@ suspicious_command_arg_space = "warn"
suspicious_xor_used_as_pow = "warn"
todo = "warn"
too_long_first_doc_paragraph = "warn"
too_many_arguments = "warn"
trailing_empty_array = "warn"
trait_duplication_in_bounds = "warn"
transmute_ptr_to_ptr = "warn"
@@ -358,6 +365,7 @@ unnecessary_safety_doc = "warn"
unnecessary_self_imports = "warn"
unnecessary_semicolon = "warn"
unnecessary_struct_initialization = "warn"
unnecessary_trailing_comma = "warn"
unnecessary_wraps = "warn"
unnested_or_patterns = "warn"
unused_async = "warn"
@@ -382,6 +390,7 @@ too_many_lines = "allow"
# These are meh:
assigning_clones = "allow" # No please
cast_possible_truncation = "allow" # too many hits
let_underscore_must_use = "allow"
let_underscore_untyped = "allow"
manual_range_contains = "allow" # this one is just worse imho

View File

@@ -3,7 +3,7 @@
# -----------------------------------------------------------------------------
# Section identical to scripts/clippy_wasm/clippy.toml:
msrv = "1.92"
msrv = "1.95"
allow-unwrap-in-tests = true

View File

@@ -232,6 +232,7 @@ web-sys = { workspace = true, features = [
"File",
"FileList",
"FocusEvent",
"FocusOptions",
"HtmlCanvasElement",
"HtmlElement",
"HtmlInputElement",

View File

@@ -122,13 +122,13 @@ impl<T: WinitApp> WinitAppWrapper<T> {
event_result
}
EventResult::RepaintNow(window_id) => {
log::trace!("RepaintNow of {window_id:?}",);
log::trace!("RepaintNow of {window_id:?}");
self.windows_next_repaint_times
.insert(window_id, Instant::now());
event_result
}
EventResult::RepaintNext(window_id) => {
log::trace!("RepaintNext of {window_id:?}",);
log::trace!("RepaintNext of {window_id:?}");
self.windows_next_repaint_times
.insert(window_id, Instant::now());
event_result

View File

@@ -316,7 +316,7 @@ impl AppRunner {
}
self.handle_platform_output(platform_output);
if is_visible {
if is_visible || !textures_delta.is_empty() {
self.textures_delta.append(textures_delta);
self.clipped_primitives = Some(self.egui_ctx.tessellate(shapes, pixels_per_point));
}
@@ -401,7 +401,7 @@ impl AppRunner {
} else {
// We are not editing text - give the focus to the canvas.
self.text_agent.blur();
self.canvas().focus().ok();
super::focus_without_scroll(self.canvas()).ok();
}
}

View File

@@ -20,6 +20,12 @@ pub(crate) struct WebInput {
/// Helps to track the delta rotation from gesture events
pub accumulated_rotation: f32,
/// The last modifier state we sent to egui.
///
/// The web has no dedicated modifier event, so we derive the state from each DOM event and
/// emit [`egui::Event::ModifiersChanged`] when it changes (see [`Self::set_modifiers`]).
pub modifiers: egui::Modifiers,
/// The raw input to `egui`.
pub raw: egui::RawInput,
}
@@ -53,11 +59,23 @@ impl WebInput {
}
// log::debug!("on_web_page_focus_change: {focused}");
self.raw.modifiers = egui::Modifiers::default(); // Avoid sticky modifier keys on alt-tab:
self.modifiers = egui::Modifiers::default(); // Avoid sticky modifier keys on alt-tab:
self.raw.focused = focused;
self.raw.events.push(egui::Event::WindowFocused(focused));
self.primary_touch = None;
}
/// Update the modifier state, emitting [`egui::Event::ModifiersChanged`] if it changed.
///
/// Call before pushing the DOM event itself so egui sees the new modifier state first.
pub fn set_modifiers(&mut self, modifiers: egui::Modifiers) {
if self.modifiers != modifiers {
self.modifiers = modifiers;
self.raw
.events
.push(egui::Event::ModifiersChanged(modifiers));
}
}
}
// ----------------------------------------------------------------------------

View File

@@ -196,15 +196,18 @@ pub(crate) fn on_keydown(event: web_sys::KeyboardEvent, runner: &mut AppRunner)
}
let modifiers = modifiers_from_kb_event(&event);
runner.input.raw.modifiers = modifiers;
runner.input.set_modifiers(modifiers);
let key = event.key();
let egui_key = translate_key(&key);
let logical_key = translate_key(&key);
let physical_key = translate_key(&event.code());
if let Some(egui_key) = egui_key {
// Fall back to the physical key so that modifier keys (which have no logical
// `egui::Key`) and non-Latin layouts still produce a `Key` event.
if let Some(active_key) = logical_key.or(physical_key) {
let egui_event = egui::Event::Key {
key: egui_key,
physical_key: None, // TODO(fornwall)
key: active_key,
physical_key,
pressed: true,
repeat: false, // egui will fill this in for us!
modifiers,
@@ -213,11 +216,11 @@ pub(crate) fn on_keydown(event: web_sys::KeyboardEvent, runner: &mut AppRunner)
runner.input.raw.events.push(egui_event);
runner.needs_repaint.repaint_asap();
let prevent_default = should_prevent_default_for_key(runner, &modifiers, egui_key);
let prevent_default = should_prevent_default_for_key(runner, &modifiers, active_key);
if false {
log::debug!(
"On keydown {:?} {egui_key:?}, has_focus: {has_focus}, egui_wants_keyboard: {}, prevent_default: {prevent_default}",
"On keydown {:?} {active_key:?}, has_focus: {has_focus}, egui_wants_keyboard: {}, prevent_default: {prevent_default}",
event.key().as_str(),
runner.egui_ctx().egui_wants_keyboard_input()
);
@@ -287,14 +290,17 @@ fn install_keyup(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsV
#[expect(clippy::needless_pass_by_value)] // So that we can pass it directly to `add_event_listener`
pub(crate) fn on_keyup(event: web_sys::KeyboardEvent, runner: &mut AppRunner) {
let modifiers = modifiers_from_kb_event(&event);
runner.input.raw.modifiers = modifiers;
runner.input.set_modifiers(modifiers);
let mut should_stop_propagation = true;
if let Some(key) = translate_key(&event.key()) {
let logical_key = translate_key(&event.key());
let physical_key = translate_key(&event.code());
if let Some(active_key) = logical_key.or(physical_key) {
let egui_event = egui::Event::Key {
key,
physical_key: None, // TODO(fornwall)
key: active_key,
physical_key,
pressed: false,
repeat: false,
modifiers,
@@ -535,11 +541,11 @@ fn install_pointerdown(runner_ref: &WebRunner, target: &EventTarget) -> Result<(
"pointerdown",
|event: web_sys::PointerEvent, runner: &mut AppRunner| {
let modifiers = modifiers_from_mouse_event(&event);
runner.input.raw.modifiers = modifiers;
runner.input.set_modifiers(modifiers);
let mut should_stop_propagation = true;
if let Some(button) = button_from_mouse_event(&event) {
let pos = pos_from_mouse_event(runner.canvas(), &event, runner.egui_ctx());
let modifiers = runner.input.raw.modifiers;
let modifiers = runner.input.modifiers;
let egui_event = egui::Event::PointerButton {
pos,
button,
@@ -572,7 +578,7 @@ fn install_pointerup(runner_ref: &WebRunner, target: &EventTarget) -> Result<(),
"pointerup",
|event: web_sys::PointerEvent, runner| {
let modifiers = modifiers_from_mouse_event(&event);
runner.input.raw.modifiers = modifiers;
runner.input.set_modifiers(modifiers);
let pos = pos_from_mouse_event(runner.canvas(), &event, runner.egui_ctx());
@@ -581,7 +587,7 @@ fn install_pointerup(runner_ref: &WebRunner, target: &EventTarget) -> Result<(),
egui::pos2(event.client_x() as f32, event.client_y() as f32),
) && let Some(button) = button_from_mouse_event(&event)
{
let modifiers = runner.input.raw.modifiers;
let modifiers = runner.input.modifiers;
let egui_event = egui::Event::PointerButton {
pos,
button,
@@ -599,7 +605,7 @@ fn install_pointerup(runner_ref: &WebRunner, target: &EventTarget) -> Result<(),
// not working when focusing on a text field in an egui app.
// This attempts to fix that by forcing the focus on any
// click on the canvas.
runner.canvas().focus().ok();
super::focus_without_scroll(runner.canvas()).ok();
// In Safari we are only allowed to do certain things
// (like playing audio, start a download, etc)
@@ -647,7 +653,7 @@ fn is_interested_in_pointer_event(runner: &AppRunner, pos: egui::Pos2) -> bool {
fn install_mousemove(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> {
runner_ref.add_event_listener(target, "mousemove", |event: web_sys::MouseEvent, runner| {
let modifiers = modifiers_from_mouse_event(&event);
runner.input.raw.modifiers = modifiers;
runner.input.set_modifiers(modifiers);
let pos = pos_from_mouse_event(runner.canvas(), &event, runner.egui_ctx());
@@ -705,7 +711,7 @@ fn install_touchstart(runner_ref: &WebRunner, target: &EventTarget) -> Result<()
pos,
button: egui::PointerButton::Primary,
pressed: true,
modifiers: runner.input.raw.modifiers,
modifiers: runner.input.modifiers,
};
should_stop_propagation = (runner.web_options.should_stop_propagation)(&egui_event);
should_prevent_default = (runner.web_options.should_prevent_default)(&egui_event);
@@ -770,7 +776,7 @@ fn install_touchend(runner_ref: &WebRunner, target: &EventTarget) -> Result<(),
pos,
button: egui::PointerButton::Primary,
pressed: false,
modifiers: runner.input.raw.modifiers,
modifiers: runner.input.modifiers,
};
should_stop_propagation &= (runner.web_options.should_stop_propagation)(&egui_event);
should_prevent_default &= (runner.web_options.should_prevent_default)(&egui_event);
@@ -832,7 +838,7 @@ fn install_wheel(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsV
let modifiers = modifiers_from_wheel_event(&event);
let egui_event = if modifiers.ctrl && !runner.input.raw.modifiers.ctrl {
let egui_event = if modifiers.ctrl && !runner.input.modifiers.ctrl {
// The browser is saying the ctrl key is down, but it isn't _really_.
// This happens on pinch-to-zoom on multitouch trackpads
// egui will treat ctrl+scroll as zoom, so it all works.

View File

@@ -84,6 +84,17 @@ pub(crate) fn has_focus<T: JsCast>(element: &T) -> bool {
try_has_focus(element).unwrap_or(false)
}
/// Focus the given element without scrolling it into view.
///
/// Scrolling the element into view would scroll the whole page when
/// the app is embedded in a larger scrollable page,
/// see <https://github.com/emilk/egui/issues/8295>.
pub(crate) fn focus_without_scroll(element: &web_sys::HtmlElement) -> Result<(), JsValue> {
let options = web_sys::FocusOptions::new();
options.set_prevent_scroll(true);
element.focus_with_options(&options)
}
/// Current time in seconds (since undefined point in time).
///
/// Monotonically increasing.

View File

@@ -4,7 +4,7 @@
use std::cell::Cell;
use wasm_bindgen::prelude::*;
use web_sys::{Document, Node};
use web_sys::Document;
use super::{AppRunner, WebRunner};
@@ -15,19 +15,23 @@ pub struct TextAgent {
impl TextAgent {
/// Attach the agent to the document.
pub fn attach(runner_ref: &WebRunner, root: Node) -> Result<Self, JsValue> {
pub fn attach(
runner_ref: &WebRunner,
canvas: &web_sys::HtmlCanvasElement,
) -> Result<Self, JsValue> {
let document = web_sys::window().unwrap().document().unwrap();
// create an `<input>` element
let input = document
.create_element("input")?
.dyn_into::<web_sys::HtmlElement>()?;
input.set_autofocus(true)?;
let input = input.dyn_into::<web_sys::HtmlInputElement>()?;
.dyn_into::<web_sys::HtmlInputElement>()?;
input.set_type("text");
input.set_attribute("autocapitalize", "off")?;
// append it to `<body>` and hide it outside of the viewport
// Hide the element, and park it over the canvas
// so that focusing it can never scroll some other part
// of the page into view.
let canvas_rect = super::canvas_content_rect(canvas);
let style = input.style();
style.set_property("background-color", "transparent")?;
style.set_property("border", "none")?;
@@ -36,11 +40,12 @@ impl TextAgent {
style.set_property("height", "1px")?;
style.set_property("caret-color", "transparent")?;
style.set_property("position", "absolute")?;
style.set_property("top", "0")?;
style.set_property("left", "0")?;
style.set_property("top", &format!("{}px", canvas_rect.min.y))?;
style.set_property("left", &format!("{}px", canvas_rect.min.x))?;
// Prevent auto-zoom on mobile browsers (requires at least 16px).
style.set_property("font-size", "16px")?;
let root = canvas.get_root_node();
if root.has_type::<Document>() {
// root object is a document, append to its body
root.dyn_into::<Document>()?
@@ -52,6 +57,13 @@ impl TextAgent {
root.append_child(&input)?;
}
// Focus the app on startup, without scrolling the page.
// We do this instead of setting the `autofocus` attribute,
// since the browser scrolls the focused element into view when
// honoring `autofocus`, and there is no way to prevent that.
// See https://github.com/emilk/egui/issues/8295
super::focus_without_scroll(&input).ok();
// attach event listeners
let on_input = {
@@ -67,7 +79,7 @@ impl TextAgent {
// between versions 14.7.09 and 17.0.12.
if !event.is_composing() {
input.blur().ok();
input.focus().ok();
super::focus_without_scroll(&input).ok();
}
if event.is_composing() {
@@ -221,7 +233,7 @@ impl TextAgent {
log::trace!("Focusing text agent");
if let Err(err) = self.input.focus() {
if let Err(err) = super::focus_without_scroll(&self.input) {
log::error!("failed to set focus: {}", super::string_from_js_value(&err));
}
}

View File

@@ -90,7 +90,7 @@ impl WebPainterWgpu {
&& create_new.display_handle.is_none()
{
// Force WebGL, useful for quick & dirty testing:
//create_new.instance_descriptor.backends = wgpu::Backends::GL;
// create_new.instance_descriptor.backends = wgpu::Backends::GL;
create_new.display_handle = Some(Box::new(WebDisplay));
}
@@ -380,7 +380,7 @@ impl WebPainter for WebPainterWgpu {
);
}
frame.present();
render_state.queue.present(frame);
}
// Free textures marked for destruction **after** queue submit since they might still be used in the current frame.

View File

@@ -73,7 +73,7 @@ impl WebRunner {
{
// First set up the app runner:
let text_agent = TextAgent::attach(self, canvas.get_root_node())?;
let text_agent = TextAgent::attach(self, &canvas)?;
let app_runner =
AppRunner::new(canvas.clone(), web_options, app_creator, text_agent).await?;
self.app_runner.replace(Some(app_runner));

View File

@@ -212,10 +212,14 @@ impl CaptureState {
let buffer_slice = buffer.slice(..);
let mut pixels = Vec::with_capacity((tex_extent.width * tex_extent.height) as usize);
for padded_row in buffer_slice
.get_mapped_range()
.chunks(padding.padded_bytes_per_row as usize)
{
let mapped_range = match buffer_slice.get_mapped_range() {
Ok(range) => range,
Err(err) => {
log::error!("Failed to get mapped range for reading: {err}");
return;
}
};
for padded_row in mapped_range.chunks(padding.padded_bytes_per_row as usize) {
let row = &padded_row[..padding.unpadded_bytes_per_row as usize];
for color in row.chunks(4) {
pixels.push(epaint::Color32::from_rgba_premultiplied(
@@ -226,6 +230,7 @@ impl CaptureState {
));
}
}
drop(mapped_range);
buffer.unmap();
tx.send((

View File

@@ -150,6 +150,7 @@ async fn request_adapter(
// * fails if there's no software rasterizer available
// * can achieve the same with `native_adapter_selector`
force_fallback_adapter: false,
apply_limit_buckets: false,
})
.await
.inspect_err(|_err| {
@@ -473,6 +474,7 @@ pub fn adapter_info_summary(info: &wgpu::AdapterInfo) -> String {
subgroup_min_size,
subgroup_max_size,
transient_saves_memory,
limit_bucket,
} = &info;
// Example values:
@@ -523,9 +525,10 @@ pub fn adapter_info_summary(info: &wgpu::AdapterInfo) -> String {
}
write!(
summary,
", transient_saves_memory: {transient_saves_memory}"
", transient_saves_memory: {transient_saves_memory:?}"
)
.ok();
write!(summary, ", limit_bucket: {limit_bucket:?}").ok();
summary
}

View File

@@ -375,14 +375,14 @@ impl Renderer {
vertex: wgpu::VertexState {
entry_point: Some("vs_main"),
module: &module,
buffers: &[wgpu::VertexBufferLayout {
buffers: &[Some(wgpu::VertexBufferLayout {
array_stride: 5 * 4,
step_mode: wgpu::VertexStepMode::Vertex,
// 0: vec2 position
// 1: vec2 texture coordinates
// 2: uint color
attributes: &wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2, 2 => Uint32],
}],
})],
compilation_options: wgpu::PipelineCompilationOptions::default()
},
primitive: wgpu::PrimitiveState {

View File

@@ -39,6 +39,7 @@ where
}
#[derive(Clone)]
#[expect(clippy::large_enum_variant)]
pub enum WgpuSetup {
/// Construct a wgpu setup using some predefined settings & heuristics.
/// This is the default option. You can customize most behaviours overriding the

View File

@@ -765,7 +765,7 @@ impl Painter {
profiling::scope!("present");
// wgpu doesn't document where vsync can happen. Maybe here?
let start = web_time::Instant::now();
output_frame.present();
render_state.queue.present(output_frame);
vsync_sec += start.elapsed().as_secs_f32();
}

View File

@@ -84,6 +84,13 @@ pub struct State {
viewport_id: ViewportId,
start_time: web_time::Instant,
egui_input: egui::RawInput,
/// The current modifier state.
///
/// We keep a copy so we can stamp
/// it onto per-event `modifiers` fields and emit [`egui::Event::ModifiersChanged`].
modifiers: egui::Modifiers,
pointer_pos_in_points: Option<egui::Pos2>,
any_pointer_button_down: bool,
current_cursor_icon: Option<egui::CursorIcon>,
@@ -146,6 +153,7 @@ impl State {
.unwrap_or_else(web_time::Instant::now),
egui_ctx,
egui_input,
modifiers: egui::Modifiers::default(),
pointer_pos_in_points: None,
any_pointer_button_down: false,
current_cursor_icon: None,
@@ -422,6 +430,10 @@ impl State {
};
self.egui_input.focused = focused;
if !focused {
// Avoid sticky modifiers when focus is lost (egui clears its own copy too).
self.modifiers = egui::Modifiers::default();
}
self.egui_input
.events
.push(egui::Event::WindowFocused(focused));
@@ -473,16 +485,20 @@ impl State {
let shift = state.shift_key();
let super_ = state.super_key();
self.egui_input.modifiers.alt = alt;
self.egui_input.modifiers.ctrl = ctrl;
self.egui_input.modifiers.shift = shift;
self.egui_input.modifiers.mac_cmd = cfg!(target_os = "macos") && super_;
self.egui_input.modifiers.command = if cfg!(target_os = "macos") {
self.modifiers.alt = alt;
self.modifiers.ctrl = ctrl;
self.modifiers.shift = shift;
self.modifiers.mac_cmd = cfg!(target_os = "macos") && super_;
self.modifiers.command = if cfg!(target_os = "macos") {
super_
} else {
ctrl
};
self.egui_input
.events
.push(egui::Event::ModifiersChanged(self.modifiers));
EventResponse {
repaint: true,
consumed: false,
@@ -541,7 +557,7 @@ impl State {
unit: egui::MouseWheelUnit::Point,
delta: Vec2::new(delta.x, delta.y) / pixels_per_point,
phase: to_egui_touch_phase(*phase),
modifiers: self.egui_input.modifiers,
modifiers: self.modifiers,
});
EventResponse {
repaint: true,
@@ -790,7 +806,7 @@ impl State {
pos,
button,
pressed,
modifiers: self.egui_input.modifiers,
modifiers: self.modifiers,
});
if self.simulate_touch_screen {
@@ -937,7 +953,7 @@ impl State {
),
};
let phase = to_egui_touch_phase(phase);
let modifiers = self.egui_input.modifiers;
let modifiers = self.modifiers;
self.egui_input.events.push(egui::Event::MouseWheel {
unit,
delta,
@@ -998,13 +1014,13 @@ impl State {
// See also: https://github.com/emilk/egui/issues/3653
if let Some(active_key) = logical_key.or(physical_key) {
if pressed {
if is_cut_command(self.egui_input.modifiers, active_key) {
if is_cut_command(self.modifiers, active_key) {
self.egui_input.events.push(egui::Event::Cut);
return;
} else if is_copy_command(self.egui_input.modifiers, active_key) {
} else if is_copy_command(self.modifiers, active_key) {
self.egui_input.events.push(egui::Event::Copy);
return;
} else if is_paste_command(self.egui_input.modifiers, active_key) {
} else if is_paste_command(self.modifiers, active_key) {
if let Some(contents) = self.clipboard.get() {
let contents = contents.replace("\r\n", "\n");
if !contents.is_empty() {
@@ -1020,7 +1036,7 @@ impl State {
physical_key,
pressed,
repeat: false, // egui will fill this in for us!
modifiers: self.egui_input.modifiers,
modifiers: self.modifiers,
});
}
@@ -1036,9 +1052,8 @@ impl State {
// We need to ignore these characters that are side-effects of commands.
// Also make sure the key is pressed (not released). On Linux, text might
// contain some data even when the key is released.
let is_cmd = self.egui_input.modifiers.ctrl
|| self.egui_input.modifiers.command
|| self.egui_input.modifiers.mac_cmd;
let is_cmd =
self.modifiers.ctrl || self.modifiers.command || self.modifiers.mac_cmd;
if pressed && !is_cmd {
self.egui_input
.events

View File

@@ -95,7 +95,7 @@ impl AnimationManager {
anim.from_value..=anim.to_value,
);
if anim.to_value != value {
anim.from_value = current_value; //start new animation from current position of playing animation
anim.from_value = current_value; // start new animation from current position of playing animation
anim.to_value = value;
anim.toggle_time = input.time;
}

View File

@@ -88,16 +88,36 @@ pub fn capture() -> String {
return false;
}
// Remove stuff that isn't user calls:
// Remove frames that are part of egui itself, since they are not useful to the user.
let skip_crates = [
"alloc",
"backtrace",
"core",
"eframe",
"egui_extras",
"egui_plot",
"egui",
"epaint",
"std",
"winit",
];
for crate_name in skip_crates {
let name = frame.name.trim_start_matches('<');
if name.starts_with(crate_name)
&& name
.as_bytes()
.get(crate_name.len())
.copied()
.is_none_or(|b| b.is_ascii_punctuation() && b != b'_')
{
return false;
}
}
let skip_prefixes = [
// "backtrace::", // not needed, since we cut at egui::callstack::capture
"egui::",
"<egui::",
"<F as egui::widgets::Widget>",
"egui_plot::",
"egui_extras::",
"core::ptr::drop_in_place<egui::ui::Ui>",
"eframe::",
"core::ops::function::FnOnce::call_once",
"<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once",
];

View File

@@ -571,6 +571,7 @@ impl<'a> Popup<'a> {
.fixed_pos(anchor)
.sense(sense)
.layout(layout)
.sizing_pass(!was_open_last_frame)
.info(info.unwrap_or_else(|| {
UiStackInfo::new(kind.into()).with_tag_value(
MenuConfig::MENU_CONFIG_TAG,

View File

@@ -3544,7 +3544,7 @@ impl Context {
if !is_visible {
continue;
}
let text = format!("{} - {:?}", layer_id.short_debug_format(), area.rect(),);
let text = format!("{} - {:?}", layer_id.short_debug_format(), area.rect());
// TODO(emilk): `Sense::hover_highlight()`
let response =
ui.add(Label::new(RichText::new(text).monospace()).sense(Sense::click()));

View File

@@ -69,6 +69,9 @@ pub enum Event {
modifiers: Modifiers,
},
/// The set of held modifier keys changed.
ModifiersChanged(Modifiers),
/// The mouse or touch moved to a new place.
PointerMoved(Pos2),

View File

@@ -1,6 +1,6 @@
use crate::{OrderedViewportIdMap, Theme, ViewportId, ViewportIdMap, emath::Rect};
use super::{DroppedFile, Event, HoveredFile, Modifiers, SafeAreaInsets, ViewportInfo};
use super::{DroppedFile, Event, HoveredFile, SafeAreaInsets, ViewportInfo};
/// What the integrations provides to egui at the start of each frame.
///
@@ -53,9 +53,6 @@ pub struct RawInput {
/// 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,
@@ -92,7 +89,6 @@ impl Default for RawInput {
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(),
@@ -127,7 +123,6 @@ impl RawInput {
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),
@@ -145,7 +140,6 @@ impl RawInput {
max_texture_side,
time,
predicted_dt,
modifiers,
mut events,
mut hovered_files,
mut dropped_files,
@@ -160,7 +154,6 @@ impl RawInput {
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);
@@ -179,7 +172,6 @@ impl RawInput {
max_texture_side,
time,
predicted_dt,
modifiers,
events,
hovered_files,
dropped_files,
@@ -210,7 +202,6 @@ impl RawInput {
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}"));

View File

@@ -384,7 +384,7 @@ impl Key {
"Escape" | "Esc" => Self::Escape,
"Tab" => Self::Tab,
"Backspace" => Self::Backspace,
"Enter" | "Return" => Self::Enter,
"Enter" | "Return" | "NumpadEnter" => Self::Enter,
"Help" | "Insert" => Self::Insert,
"Delete" => Self::Delete,
@@ -399,19 +399,19 @@ impl Key {
" " | "Space" => Self::Space,
":" | "Colon" => Self::Colon,
"," | "Comma" => Self::Comma,
"-" | "" | "Minus" => Self::Minus,
"." | "Period" => Self::Period,
"+" | "Plus" => Self::Plus,
"," | "Comma" | "NumpadComma" => Self::Comma,
"-" | "" | "Minus" | "NumpadSubtract" => Self::Minus,
"." | "Period" | "NumpadDecimal" => Self::Period,
"+" | "Plus" | "NumpadAdd" => Self::Plus,
"=" | "Equal" | "Equals" | "NumpadEqual" => Self::Equals,
";" | "Semicolon" => Self::Semicolon,
"\\" | "Backslash" => Self::Backslash,
"/" | "Slash" => Self::Slash,
"/" | "Slash" | "NumpadDivide" => Self::Slash,
"|" | "Pipe" => Self::Pipe,
"?" | "Questionmark" => Self::Questionmark,
"!" | "Exclamationmark" => Self::Exclamationmark,
"[" | "OpenBracket" => Self::OpenBracket,
"]" | "CloseBracket" => Self::CloseBracket,
"[" | "OpenBracket" | "BracketLeft" => Self::OpenBracket,
"]" | "CloseBracket" | "BracketRight" => Self::CloseBracket,
"{" | "OpenCurlyBracket" => Self::OpenCurlyBracket,
"}" | "CloseCurlyBracket" => Self::CloseCurlyBracket,
"`" | "Backtick" | "Backquote" | "Grave" => Self::Backtick,
@@ -428,32 +428,32 @@ impl Key {
"8" | "Digit8" | "Numpad8" => Self::Num8,
"9" | "Digit9" | "Numpad9" => Self::Num9,
"a" | "A" => Self::A,
"b" | "B" => Self::B,
"c" | "C" => Self::C,
"d" | "D" => Self::D,
"e" | "E" => Self::E,
"f" | "F" => Self::F,
"g" | "G" => Self::G,
"h" | "H" => Self::H,
"i" | "I" => Self::I,
"j" | "J" => Self::J,
"k" | "K" => Self::K,
"l" | "L" => Self::L,
"m" | "M" => Self::M,
"n" | "N" => Self::N,
"o" | "O" => Self::O,
"p" | "P" => Self::P,
"q" | "Q" => Self::Q,
"r" | "R" => Self::R,
"s" | "S" => Self::S,
"t" | "T" => Self::T,
"u" | "U" => Self::U,
"v" | "V" => Self::V,
"w" | "W" => Self::W,
"x" | "X" => Self::X,
"y" | "Y" => Self::Y,
"z" | "Z" => Self::Z,
"a" | "A" | "KeyA" => Self::A,
"b" | "B" | "KeyB" => Self::B,
"c" | "C" | "KeyC" => Self::C,
"d" | "D" | "KeyD" => Self::D,
"e" | "E" | "KeyE" => Self::E,
"f" | "F" | "KeyF" => Self::F,
"g" | "G" | "KeyG" => Self::G,
"h" | "H" | "KeyH" => Self::H,
"i" | "I" | "KeyI" => Self::I,
"j" | "J" | "KeyJ" => Self::J,
"k" | "K" | "KeyK" => Self::K,
"l" | "L" | "KeyL" => Self::L,
"m" | "M" | "KeyM" => Self::M,
"n" | "N" | "KeyN" => Self::N,
"o" | "O" | "KeyO" => Self::O,
"p" | "P" | "KeyP" => Self::P,
"q" | "Q" | "KeyQ" => Self::Q,
"r" | "R" | "KeyR" => Self::R,
"s" | "S" | "KeyS" => Self::S,
"t" | "T" | "KeyT" => Self::T,
"u" | "U" | "KeyU" => Self::U,
"v" | "V" | "KeyV" => Self::V,
"w" | "W" | "KeyW" => Self::W,
"x" | "X" | "KeyX" => Self::X,
"y" | "Y" | "KeyY" => Self::Y,
"z" | "Z" | "KeyZ" => Self::Z,
"F1" => Self::F1,
"F2" => Self::F2,
@@ -499,8 +499,9 @@ impl Key {
"ControlRight" => Self::ControlRight,
"AltLeft" => Self::AltLeft,
"AltRight" => Self::AltRight,
"SuperLeft" => Self::SuperLeft,
"SuperRight" => Self::SuperRight,
"SuperLeft" | "MetaLeft" | "OSLeft" => Self::SuperLeft,
"SuperRight" | "MetaRight" | "OSRight" => Self::SuperRight,
"IntlBackslash" => Self::IntlBackslash,

View File

@@ -201,8 +201,6 @@ fn contains_circle(interact_rect: emath::Rect, pos: Pos2, radius: f32) -> bool {
}
fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits {
#![expect(clippy::collapsible_else_if)]
// First find the best direct hits:
let hit_click = find_closest_within(
close.iter().copied().filter(|w| w.sense.senses_click()),

View File

@@ -393,6 +393,7 @@ impl InputState {
let pointer = self.pointer.begin_pass(time, &new, options);
let mut keys_down = self.keys_down;
let mut modifiers = self.modifiers;
let mut zoom_factor_delta = 1.0; // TODO(emilk): smoothing for zoom factor
let mut rotation_radians = 0.0;
@@ -429,6 +430,9 @@ impl InputState {
*modifiers,
);
}
Event::ModifiersChanged(new_modifiers) => {
modifiers = *new_modifiers;
}
Event::Zoom(factor) => {
zoom_factor_delta *= *factor;
}
@@ -442,6 +446,7 @@ impl InputState {
// So we take the safe route and just clear all the keys and modifiers when
// the app loses focus.
keys_down.clear();
modifiers = Modifiers::default();
}
_ => {}
}
@@ -482,7 +487,7 @@ impl InputState {
predicted_dt: new.predicted_dt,
stable_dt,
focused: new.focused,
modifiers: new.modifiers,
modifiers,
keys_down,
events: new.events.clone(), // TODO(emilk): remove clone() and use raw.events
raw: new,

View File

@@ -3,7 +3,7 @@
//! Try the live web demo: <https://www.egui.rs/#demo>. Read more about egui at <https://github.com/emilk/egui>.
//!
//! `egui` is in heavy development, with each new version having breaking changes.
//! You need to have rust 1.92.0 or later to use `egui`.
//! You need to have rust 1.95.0 or later to use `egui`.
//!
//! To quickly get started with egui, you can take a look at [`eframe_template`](https://github.com/emilk/eframe_template)
//! which uses [`eframe`](https://docs.rs/eframe).

View File

@@ -1460,9 +1460,9 @@ fn order_map_total_ordering() {
// Assert that `areas.compare_order()` forms a total ordering
let mut equivalence_classes = vec![0];
let mut i = 0;
for l in layers.windows(2) {
assert!(l[0].order <= l[1].order, "does not follow LayerId.order");
if areas.compare_order(l[0], l[1]) != std::cmp::Ordering::Equal {
for &[a, b] in layers.array_windows() {
assert!(a.order <= b.order, "does not follow LayerId.order");
if areas.compare_order(a, b) != std::cmp::Ordering::Equal {
i += 1;
}
equivalence_classes.push(i);

View File

@@ -1395,7 +1395,7 @@ impl Default for DebugOptions {
show_resize: false,
show_interactive_widgets: false,
show_widget_hits: false,
warn_if_rect_changes_id: cfg!(debug_assertions),
warn_if_rect_changes_id: false,
show_unaligned: cfg!(debug_assertions),
show_focused_widget: false,
}

View File

@@ -134,9 +134,8 @@ where
if self.has_undo(current_state) {
self.flux = None;
if self.undos.back() == Some(current_state) {
#[expect(clippy::unwrap_used)] // we just checked that undos is not empty
self.redos.push(self.undos.pop_back().unwrap());
if let Some(state) = self.undos.pop_back_if(|state| &*state == current_state) {
self.redos.push(state);
} else {
self.redos.push(current_state.clone());
}

View File

@@ -223,6 +223,7 @@ fn integration_ui(ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
subgroup_min_size,
subgroup_max_size,
transient_saves_memory,
limit_bucket,
} = &info;
// Example values:
@@ -276,7 +277,11 @@ fn integration_ui(ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
ui.end_row();
}
ui.label("Transient saves memory:");
ui.label(format!("{transient_saves_memory}"));
ui.label(format!("{transient_saves_memory:?}"));
ui.end_row();
ui.label("Limit bucket:");
ui.label(format!("{limit_bucket:?}"));
ui.end_row();
});
};

View File

@@ -56,6 +56,9 @@ enum Phase {
/// A screenshot was dispatched with this `user_data` id; reply when the matching
/// [`egui::Event::Screenshot`] arrives.
AwaitScreenshot { id: u64 },
/// Watching for the app to go idle.
Settle { steps_taken: u64, max_steps: u64 },
}
struct InFlight {
@@ -116,7 +119,12 @@ impl InspectionPlugin {
/// While requests are still in flight, keep the UI loop spinning — reactive apps would
/// otherwise go idle between hooks before a screenshot round-trips.
fn maybe_repaint(&self, ctx: &Context) {
if !self.in_flight.is_empty() {
// Don't repaint if there's only a `Request::Settle`.
if self
.in_flight
.iter()
.any(|item| !matches!(item.req, Request::Settle { .. }))
{
ctx.request_repaint();
}
}
@@ -236,6 +244,13 @@ impl egui::Plugin for InspectionPlugin {
item.phase = Phase::AwaitScreenshot { id };
true
}
Request::Settle { max_steps } => {
item.phase = Phase::Settle {
steps_taken: 0,
max_steps: *max_steps,
};
true
}
}
});
self.next_screenshot_id = next_id;
@@ -249,9 +264,14 @@ impl egui::Plugin for InspectionPlugin {
return;
}
let immediate_repaint = output
.viewport_output
.values()
.any(|viewport| viewport.repaint_delay == Duration::ZERO);
let step = self.step;
self.in_flight
.retain_mut(|item| match (&item.phase, &item.req) {
.retain_mut(|item| match (&mut item.phase, &item.req) {
(Phase::AwaitOutput, Request::GetTree) => {
if let Some(reply) = item.reply.take() {
reply(Response::Tree {
@@ -268,6 +288,27 @@ impl egui::Plugin for InspectionPlugin {
}
false
}
(
Phase::Settle {
steps_taken,
max_steps,
},
Request::Settle { .. },
) => {
*steps_taken += 1;
let steps_exceeded = *steps_taken >= *max_steps;
if !immediate_repaint || steps_exceeded {
if let Some(reply) = item.reply.take() {
reply(Response::Settled {
settled: !immediate_repaint,
steps: *steps_taken,
});
}
false
} else {
true
}
}
_ => true,
});

View File

@@ -63,6 +63,11 @@ pub enum Request {
/// (via [`egui::ViewportCommand::InnerSize`]). Reply: [`Response::Done`]. This is the one
/// action that isn't expressible as an [`egui::Event`].
Resize { width: u32, height: u32 },
/// Wait until the app goes idle, then reply [`Response::Settled`].
///
/// Will wait for at most `max_steps`.
Settle { max_steps: u64 },
}
/// Sent peer → inspector, exactly one per [`Request`].
@@ -99,6 +104,15 @@ pub enum Response {
/// (not merely received): the events were processed by a frame, or the resize dispatched.
Done,
/// Reply to [`Request::Settle`].
Settled {
/// Did we settle within `max_steps`?
settled: bool,
/// How many frames did we run until we settled?
steps: u64,
},
/// The peer failed to service the request (recoverable; the connection stays open).
Error { message: String },
}

View File

@@ -175,10 +175,9 @@ impl<'a, State> Harness<'a, State> {
#[cfg(feature = "snapshot")]
snapshot_results: SnapshotResults::default(),
};
// Fulfill any screenshot requested during the initial frame above (which didn't go
// through `_step`).
#[cfg(any(feature = "wgpu", feature = "snapshot"))]
harness.handle_screenshots();
// Handle any viewport commands (e.g. a screenshot or resize) requested during the initial
// frame above (which didn't go through `_step`).
harness.handle_viewport_commands();
// Run the harness until it is stable, ensuring that all Areas are shown and animations are done
harness.run_ok();
@@ -251,14 +250,7 @@ impl<'a, State> Harness<'a, State> {
self._step(false);
}
for event in events {
match event {
EventType::Event(event) => {
self.input.events.push(event);
}
EventType::Modifiers(modifiers) => {
self.input.modifiers = modifiers;
}
}
self._step(false);
}
}
@@ -280,8 +272,7 @@ impl<'a, State> Harness<'a, State> {
self.renderer.handle_delta(&output.textures_delta);
self.output = output;
#[cfg(any(feature = "wgpu", feature = "snapshot"))]
self.handle_screenshots();
self.handle_viewport_commands();
}
/// Calculate the rect that includes all popups and tooltips.
@@ -471,7 +462,7 @@ impl<'a, State> Harness<'a, State> {
/// Queue an event to be processed in the next frame.
pub fn event(&self, event: egui::Event) {
self.queued_events.lock().push(EventType::Event(event));
self.queued_events.lock().push(event);
}
/// Queue an event with modifiers.
@@ -479,15 +470,15 @@ impl<'a, State> Harness<'a, State> {
/// Queues the modifiers to be pressed, then the event, then the modifiers to be released.
pub fn event_modifiers(&self, event: egui::Event, modifiers: Modifiers) {
let mut queue = self.queued_events.lock();
queue.push(EventType::Modifiers(modifiers));
queue.push(EventType::Event(event));
queue.push(EventType::Modifiers(Modifiers::default()));
queue.push(egui::Event::ModifiersChanged(modifiers));
queue.push(event);
queue.push(egui::Event::ModifiersChanged(Modifiers::default()));
}
fn modifiers(&self, modifiers: Modifiers) {
self.queued_events
.lock()
.push(EventType::Modifiers(modifiers));
.push(egui::Event::ModifiersChanged(modifiers));
}
pub fn key_down(&self, key: egui::Key) {
@@ -675,6 +666,36 @@ impl<'a, State> Harness<'a, State> {
self.renderer.render(&self.ctx, &output)
}
/// Apply the [`egui::ViewportCommand`]s the app emitted during the last frame.
fn handle_viewport_commands(&mut self) {
self.handle_inner_size();
#[cfg(any(feature = "wgpu", feature = "snapshot"))]
self.handle_screenshots();
}
/// Resize the harness to the last [`egui::ViewportCommand::InnerSize`] requested by the app
/// during the last frame, if any.
fn handle_inner_size(&mut self) {
let new_inner_size =
self.root_viewport_output()
.commands
.iter()
.rev()
.find_map(|command| {
if let egui::ViewportCommand::InnerSize(size) = command {
Some(*size)
} else {
None
}
});
if let Some(size) = new_inner_size {
self.set_size(size);
self.ctx.request_repaint();
}
}
/// Fulfill any [`egui::ViewportCommand::Screenshot`] requests made by the app during the
/// last frame.
///
@@ -735,10 +756,11 @@ impl<'a, State> Harness<'a, State> {
/// The root node of the test harness.
pub fn root(&self) -> Node<'_> {
Node {
accesskit_node: self.kittest.root(),
queue: &self.queued_events,
}
Node::new(
self.kittest.root(),
&self.queued_events,
self.ctx.pixels_per_point(),
)
}
/// Spawn a real native eframe window running this harness's app, reusing its [`egui::Context`].

View File

@@ -4,17 +4,13 @@ use egui::{Modifiers, PointerButton, Pos2, accesskit};
use kittest::{AccessKitNode, NodeT, debug_fmt_node};
use std::fmt::{Debug, Formatter};
pub(crate) enum EventType {
Event(egui::Event),
Modifiers(Modifiers),
}
pub(crate) type EventQueue = Mutex<Vec<EventType>>;
pub type EventQueue = Mutex<Vec<egui::Event>>;
#[derive(Clone, Copy)]
pub struct Node<'tree> {
pub(crate) accesskit_node: AccessKitNode<'tree>,
pub(crate) queue: &'tree EventQueue,
pub(crate) pixels_per_point: f32,
}
impl Debug for Node<'_> {
@@ -29,20 +25,32 @@ impl<'tree> NodeT<'tree> for Node<'tree> {
}
fn new_related(&self, child_node: AccessKitNode<'tree>) -> Self {
Self {
queue: self.queue,
accesskit_node: child_node,
}
Self::new(child_node, self.queue, self.pixels_per_point)
}
}
impl Node<'_> {
impl<'tree> Node<'tree> {
/// Construct a new accesskit node
pub fn new(
accesskit_node: AccessKitNode<'tree>,
queue: &'tree EventQueue,
pixels_per_point: f32,
) -> Self {
Self {
accesskit_node,
queue,
pixels_per_point,
}
}
fn event(&self, event: egui::Event) {
self.queue.lock().push(EventType::Event(event));
self.queue.lock().push(event);
}
fn modifiers(&self, modifiers: Modifiers) {
self.queue.lock().push(EventType::Modifiers(modifiers));
self.queue
.lock()
.push(egui::Event::ModifiersChanged(modifiers));
}
pub fn hover(&self) {
@@ -104,14 +112,17 @@ impl Node<'_> {
));
}
/// This returns the rect in logical ui coordinates while the underlying [`accesskit::Node`] has it
/// in physical screen coordinates.
pub fn rect(&self) -> egui::Rect {
let rect = self
.accesskit_node
.bounding_box()
.expect("Every egui node should have a rect");
let ppp = self.pixels_per_point;
egui::Rect {
min: Pos2::new(rect.x0 as f32, rect.y0 as f32),
max: Pos2::new(rect.x1 as f32, rect.y1 as f32),
min: Pos2::new(rect.x0 as f32 / ppp, rect.y0 as f32 / ppp),
max: Pos2::new(rect.x1 as f32 / ppp, rect.y1 as f32 / ppp),
}
}

View File

@@ -58,7 +58,9 @@ pub(crate) fn texture_to_image(device: &Device, queue: &Queue, texture: &Texture
receiver.recv().unwrap().unwrap();
let buffer_slice = output_buffer.slice(..);
let data = buffer_slice.get_mapped_range();
let data = buffer_slice
.get_mapped_range()
.expect("Failed to get mapped range");
let data = data
.chunks_exact(buffer_dimensions.padded_bytes_per_row)
.flat_map(|row| row.iter().take(buffer_dimensions.unpadded_bytes_per_row))

View File

@@ -1,5 +1,72 @@
use egui::{Align, Layout, Popup};
use egui_kittest::Harness;
use kittest::Queryable as _;
#[test]
fn reopened_popup_resizes_for_wider_items() {
const POPUP_BUTTON: &str = "Dynamic popup";
const SHORT_ITEM: &str = "Short item";
const WIDE_ITEM: &str = "Newly added item with a much wider label";
#[derive(Default)]
struct State {
open: bool,
show_wide_item: bool,
}
let mut harness = Harness::builder()
.with_size(egui::Vec2::new(500.0, 300.0))
.build_ui_state(
|ui, state| {
let response = ui.button(POPUP_BUTTON);
if response.clicked() {
state.open = !state.open;
}
Popup::from_response(&response)
.open(state.open)
.layout(Layout::top_down_justified(Align::Min))
.show(|ui| {
_ = ui.selectable_label(false, SHORT_ITEM);
_ = ui.selectable_label(false, "Another short item");
if state.show_wide_item {
_ = ui.selectable_label(false, WIDE_ITEM);
}
});
},
State::default(),
);
harness.get_by_label(POPUP_BUTTON).click();
harness.run();
let initial_row_size = harness.get_by_label(SHORT_ITEM).rect().size();
harness.get_by_label(POPUP_BUTTON).click();
harness.run();
assert!(harness.query_by_label(SHORT_ITEM).is_none());
harness.state_mut().show_wide_item = true;
harness.run();
harness.get_by_label(POPUP_BUTTON).click();
harness.run();
let reopened_row_size = harness.get_by_label(SHORT_ITEM).rect().size();
let wide_row_size = harness.get_by_label(WIDE_ITEM).rect().size();
assert!(
reopened_row_size.x > initial_row_size.x,
"reopened row width ({}) did not grow beyond its initial width ({})",
reopened_row_size.x,
initial_row_size.x
);
assert!(
wide_row_size.y <= initial_row_size.y + 0.5,
"new row height ({}) exceeds the single-line row height ({})",
wide_row_size.y,
initial_row_size.y
);
}
#[test]
fn test_interactive_tooltip() {
struct State {

View File

@@ -262,3 +262,34 @@ fn test_ime_composition_visuals() {
harness.run();
harness.snapshot("test_ime_composition_visuals_cursor");
}
#[test]
fn inner_size_viewport_command() {
let new_size = Vec2::new(300.0, 200.0);
#[derive(Default)]
struct State {
requested: bool,
observed_size: Option<Vec2>,
}
let mut harness = Harness::builder()
.with_size(Vec2::new(100.0, 80.0))
.build_ui_state(
|ui, state: &mut State| {
// Request the resize once.
if !state.requested {
state.requested = true;
ui.ctx()
.send_viewport_cmd(egui::ViewportCommand::InnerSize(new_size));
}
state.observed_size = Some(ui.ctx().viewport_rect().size());
},
State::default(),
);
harness.run();
assert_eq!(harness.state().observed_size, Some(new_size));
}

View File

@@ -160,16 +160,14 @@ where
while self.values.len() > self.max_len {
self.values.pop_front();
}
while self.values.len() > self.min_len {
if let Some((front_time, _)) = self.values.front() {
if *front_time < now - (self.max_age as f64) {
self.values.pop_front();
} else {
break;
}
} else {
break;
}
let oldest_allowed_time = now - self.max_age as f64;
while self.min_len < self.values.len()
&& self
.values
.pop_front_if(|&mut (front_time, _)| front_time < oldest_allowed_time)
.is_some()
{
// Keep popping while the oldest sample is too old.
}
}
}

View File

@@ -86,7 +86,7 @@ impl CubicBezierShape {
/// Logical bounding rectangle (ignoring stroke width)
pub fn logical_bounding_rect(&self) -> Rect {
//temporary solution
// temporary solution
let (mut min_x, mut max_x) = if self.points[0].x < self.points[3].x {
(self.points[0].x, self.points[3].x)
} else {
@@ -793,7 +793,7 @@ mod tests {
assert!((bbox.max.x - 180.0).abs() < 0.01);
assert!((bbox.max.y - 170.0).abs() < 0.01);
let mut result = vec![curve.points[0]]; //add the start point
let mut result = vec![curve.points[0]]; // add the start point
curve.for_each_flattened_with_t(0.1, &mut |pos, _t| {
result.push(pos);
});
@@ -817,7 +817,7 @@ mod tests {
assert!((bbox.max.x - 130.42).abs() < 0.01);
assert!((bbox.max.y - 170.0).abs() < 0.01);
let mut result = vec![curve.points[0]]; //add the start point
let mut result = vec![curve.points[0]]; // add the start point
curve.for_each_flattened_with_t(0.1, &mut |pos, _t| {
result.push(pos);
});
@@ -837,28 +837,28 @@ mod tests {
fill: Default::default(),
stroke: Default::default(),
};
let mut result = vec![curve.points[0]]; //add the start point
let mut result = vec![curve.points[0]]; // add the start point
curve.for_each_flattened_with_t(1.0, &mut |pos, _t| {
result.push(pos);
});
assert_eq!(result.len(), 9);
let mut result = vec![curve.points[0]]; //add the start point
let mut result = vec![curve.points[0]]; // add the start point
curve.for_each_flattened_with_t(0.1, &mut |pos, _t| {
result.push(pos);
});
assert_eq!(result.len(), 25);
let mut result = vec![curve.points[0]]; //add the start point
let mut result = vec![curve.points[0]]; // add the start point
curve.for_each_flattened_with_t(0.01, &mut |pos, _t| {
result.push(pos);
});
assert_eq!(result.len(), 77);
let mut result = vec![curve.points[0]]; //add the start point
let mut result = vec![curve.points[0]]; // add the start point
curve.for_each_flattened_with_t(0.001, &mut |pos, _t| {
result.push(pos);
});
@@ -938,35 +938,35 @@ mod tests {
stroke: Default::default(),
};
let mut result = vec![curve.points[0]]; //add the start point
let mut result = vec![curve.points[0]]; // add the start point
curve.for_each_flattened_with_t(1.0, &mut |pos, _t| {
result.push(pos);
});
assert_eq!(result.len(), 10);
let mut result = vec![curve.points[0]]; //add the start point
let mut result = vec![curve.points[0]]; // add the start point
curve.for_each_flattened_with_t(0.5, &mut |pos, _t| {
result.push(pos);
});
assert_eq!(result.len(), 13);
let mut result = vec![curve.points[0]]; //add the start point
let mut result = vec![curve.points[0]]; // add the start point
curve.for_each_flattened_with_t(0.1, &mut |pos, _t| {
result.push(pos);
});
assert_eq!(result.len(), 28);
let mut result = vec![curve.points[0]]; //add the start point
let mut result = vec![curve.points[0]]; // add the start point
curve.for_each_flattened_with_t(0.01, &mut |pos, _t| {
result.push(pos);
});
assert_eq!(result.len(), 83);
let mut result = vec![curve.points[0]]; //add the start point
let mut result = vec![curve.points[0]]; // add the start point
curve.for_each_flattened_with_t(0.001, &mut |pos, _t| {
result.push(pos);
});
@@ -988,7 +988,7 @@ mod tests {
stroke: Default::default(),
};
let mut result = vec![curve.points[0]]; //add the start point
let mut result = vec![curve.points[0]]; // add the start point
curve.for_each_flattened_with_t(0.01, &mut |pos, _t| {
result.push(pos);
});
@@ -1007,7 +1007,7 @@ mod tests {
stroke: Default::default(),
};
let mut result = vec![curve.points[0]]; //add the start point
let mut result = vec![curve.points[0]]; // add the start point
curve.for_each_flattened_with_t(0.01, &mut |pos, _t| {
result.push(pos);
});
@@ -1026,7 +1026,7 @@ mod tests {
stroke: Default::default(),
};
let mut result = vec![curve.points[0]]; //add the start point
let mut result = vec![curve.points[0]]; // add the start point
curve.for_each_flattened_with_t(0.01, &mut |pos, _t| {
result.push(pos);
});
@@ -1045,7 +1045,7 @@ mod tests {
stroke: Default::default(),
};
let mut result = vec![curve.points[0]]; //add the start point
let mut result = vec![curve.points[0]]; // add the start point
curve.for_each_flattened_with_t(0.01, &mut |pos, _t| {
result.push(pos);
});
@@ -1064,7 +1064,7 @@ mod tests {
stroke: Default::default(),
};
let mut result = vec![curve.points[0]]; //add the start point
let mut result = vec![curve.points[0]]; // add the start point
curve.for_each_flattened_with_t(0.01, &mut |pos, _t| {
result.push(pos);
});
@@ -1083,7 +1083,7 @@ mod tests {
stroke: Default::default(),
};
let mut result = vec![curve.points[0]]; //add the start point
let mut result = vec![curve.points[0]]; // add the start point
curve.for_each_flattened_with_t(0.01, &mut |pos, _t| {
result.push(pos);
});
@@ -1100,34 +1100,34 @@ mod tests {
stroke: Default::default(),
};
let mut result = vec![curve.points[0]]; //add the start point
let mut result = vec![curve.points[0]]; // add the start point
curve.for_each_flattened_with_t(1.0, &mut |pos, _t| {
result.push(pos);
});
assert_eq!(result.len(), 9);
let mut result = vec![curve.points[0]]; //add the start point
let mut result = vec![curve.points[0]]; // add the start point
curve.for_each_flattened_with_t(0.5, &mut |pos, _t| {
result.push(pos);
});
assert_eq!(result.len(), 11);
let mut result = vec![curve.points[0]]; //add the start point
let mut result = vec![curve.points[0]]; // add the start point
curve.for_each_flattened_with_t(0.1, &mut |pos, _t| {
result.push(pos);
});
assert_eq!(result.len(), 24);
let mut result = vec![curve.points[0]]; //add the start point
let mut result = vec![curve.points[0]]; // add the start point
curve.for_each_flattened_with_t(0.01, &mut |pos, _t| {
result.push(pos);
});
assert_eq!(result.len(), 72);
let mut result = vec![curve.points[0]]; //add the start point
let mut result = vec![curve.points[0]]; // add the start point
curve.for_each_flattened_with_t(0.001, &mut |pos, _t| {
result.push(pos);
});

View File

@@ -512,8 +512,7 @@ fn points_from_line(
shapes: &mut Vec<Shape>,
) {
let mut position_on_segment = 0.0;
for window in path.windows(2) {
let (start, end) = (window[0], window[1]);
for &[start, end] in path.array_windows() {
let vector = end - start;
let segment_length = vector.length();
while position_on_segment < segment_length {
@@ -545,8 +544,7 @@ fn dashes_from_line(
let mut drawing_dash = false;
let mut step = 0;
let steps = dash_lengths.len();
for window in path.windows(2) {
let (start, end) = (window[0], window[1]);
for &[start, end] in path.array_windows() {
let vector = end - start;
let segment_length = vector.length();

View File

@@ -117,7 +117,6 @@ impl SubpixelBin {
let trunc = pos as i32;
let fract = pos - trunc as f32;
#[expect(clippy::collapsible_else_if)]
if pos.is_sign_negative() {
if fract > -0.125 {
(trunc, Self::Zero)

View File

@@ -471,8 +471,7 @@ fn layout_section(
// Process each paragraph segment (split on newlines — the shaper can't handle them).
for (seg_idx, segment) in SplitOrWhole::new(section_text, job.break_on_newline).enumerate() {
if 0 < seg_idx {
out_paragraphs.push(Paragraph::from_section_index(section_index));
paragraph = out_paragraphs.last_mut().unwrap();
paragraph = out_paragraphs.push_mut(Paragraph::from_section_index(section_index));
paragraph.empty_paragraph_height = line_height;
ctx.is_first_glyph_in_section = true;
}
@@ -1864,7 +1863,7 @@ mod tests {
// Verify that Row::text() reconstructs the input text.
let row_text: String = galley.rows.iter().map(|r| r.text()).collect();
assert_eq!(row_text, text, "Row::text() mismatch for {text:?}",);
assert_eq!(row_text, text, "Row::text() mismatch for {text:?}");
// Verify cursor round-trip: end cursor index == char count.
assert_eq!(

View File

@@ -33,6 +33,10 @@ version = 2
ignore = [
"RUSTSEC-2024-0320", # unmaintained yaml-rust pulled in by syntect
"RUSTSEC-2025-0141", # https://rustsec.org/advisories/RUSTSEC-2025-0141 - bincode is unmaintained - https://git.sr.ht/~stygianentity/bincode/tree/v3.0/item/README.md
"RUSTSEC-2026-0192", # ttf-parser is unmaintained. Only brought in via winit/sctk-adwaita (wayland window frame rendering)
"RUSTSEC-2026-0194", # quick-xml DoS - fix is in >=0.41, but held back transitively by zbus_xml (accesskit) and wayland-scanner (winit)
"RUSTSEC-2026-0195", # quick-xml DoS - same as above
"RUSTSEC-2026-0206", # rustybuzz is unmaintained. Brought in via resvg. TODO(linebender/resvg#922): Remove once the PR lands and is released
]
[bans]
@@ -52,11 +56,13 @@ skip = [
{ name = "foldhash" }, # pulled by the duplicated hashbrown versions
{ name = "getrandom" }, # ring / rustls (and thus ehttp) still depend on getrandom 0.2
{ name = "hashbrown" }, # wgpu's naga depends on 0.16, accesskit depends on 0.15
{ name = "itertools" }, # puffin 0.20 depends on 0.14, criterion 0.8 on 0.13
{ name = "jni-sys" }, # 0.3.1 depends on 0.4
{ name = "kurbo" }, # Old version because of resvg
{ name = "phf" }, # mime_guess2 -> 0.11, unicode_names2 -> 0.13
{ name = "phf_generator" }, # same phf split as phf
{ name = "phf_shared" }, # same phf split as phf
{ name = "rand_core" }, # rand ecosystem is migrating 0.8 -> 0.10; phf_generator's rand 0.8 needs rand_core 0.6, getrandom 0.4/chacha20 need rand_core 0.10
{ name = "redox_syscall" }, # old version via winit
{ name = "rustc-hash" }, # Small enough
{ name = "thiserror" }, # ecosystem is in the process of migrating from 1.x to 2.x
@@ -111,6 +117,4 @@ license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }]
unknown-registry = "deny"
unknown-git = "deny"
allow-git = [
"https://github.com/rerun-io/kittest", # TODO(lucasmerlin): remove this once the kittest crate is published"
]
allow-git = []

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2024"
rust-version = "1.92"
rust-version = "1.95"
publish = false
[lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2024"
rust-version = "1.92"
rust-version = "1.95"
publish = false
[lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2024"
rust-version = "1.92"
rust-version = "1.95"
publish = false
[lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["tami5 <kkharji@proton.me>"]
license = "MIT OR Apache-2.0"
edition = "2024"
rust-version = "1.92"
rust-version = "1.95"
publish = false
[lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Varphone Wong <varphone@qq.com>"]
license = "MIT OR Apache-2.0"
edition = "2024"
rust-version = "1.92"
rust-version = "1.95"
publish = false
[lints]

View File

@@ -3,7 +3,7 @@ name = "custom_style"
version = "0.1.0"
license = "MIT OR Apache-2.0"
edition = "2024"
rust-version = "1.92"
rust-version = "1.95"
publish = false
[lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2024"
rust-version = "1.92"
rust-version = "1.95"
publish = false
[lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Will Brown <opensource@rebeagle.com>"]
license = "MIT OR Apache-2.0"
edition = "2024"
rust-version = "1.92"
rust-version = "1.95"
publish = false
[lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Will Brown <opensource@rebeagle.com>"]
license = "MIT OR Apache-2.0"
edition = "2024"
rust-version = "1.92"
rust-version = "1.95"
publish = false
[lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2024"
rust-version = "1.92"
rust-version = "1.95"
publish = false
[lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2024"
rust-version = "1.92"
rust-version = "1.95"
publish = false
[lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2024"
rust-version = "1.92"
rust-version = "1.95"
publish = false
# `unsafe_code` is required for `#[no_mangle]`, disable workspace lints to workaround lint error.

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2024"
rust-version = "1.92"
rust-version = "1.95"
publish = false
[lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Maxim Osipenko <maxim1999max@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2024"
rust-version = "1.92"
rust-version = "1.95"
publish = false
[lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2024"
rust-version = "1.92"
rust-version = "1.95"
publish = false
[lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Jan Procházka <github.com/jprochazk>"]
license = "MIT OR Apache-2.0"
edition = "2024"
rust-version = "1.92"
rust-version = "1.95"
publish = false
[lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Jose Palazon <jose@palako.com>"]
license = "MIT OR Apache-2.0"
edition = "2024"
rust-version = "1.92"
rust-version = "1.95"
publish = false
[lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2024"
rust-version = "1.92"
rust-version = "1.95"
publish = false
[lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2024"
rust-version = "1.92"
rust-version = "1.95"
publish = false
[package.metadata.cargo-shear]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["René Rössler <rene@freshx.de>", "Andreas Faber <andreas.mfaber@gmail.com"]
license = "MIT OR Apache-2.0"
edition = "2024"
rust-version = "1.92"
rust-version = "1.95"
publish = false
[lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2024"
rust-version = "1.92"
rust-version = "1.95"
publish = false
[lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["TicClick <ya@ticclick.ch>"]
license = "MIT OR Apache-2.0"
edition = "2024"
rust-version = "1.92"
rust-version = "1.95"
publish = false
[lints]

View File

@@ -5,6 +5,6 @@
# to the user in the error, instead of "error: invalid channel name '[toolchain]'".
[toolchain]
channel = "1.92.0"
channel = "1.95.0"
components = ["rustfmt", "clippy"]
targets = ["wasm32-unknown-unknown"]

View File

@@ -9,7 +9,7 @@ set -x
# Checks all tests, lints etc.
# Basically does what the CI does.
# cargo +1.92.0 install --quiet typos-cli
# cargo +1.95.0 install --quiet typos-cli
export RUSTFLAGS="-D warnings"
export RUSTDOCFLAGS="-D warnings" # https://github.com/emilk/egui/pull/1454

View File

@@ -6,7 +6,7 @@
# -----------------------------------------------------------------------------
# Section identical to the root clippy.toml:
msrv = "1.92"
msrv = "1.95"
allow-unwrap-in-tests = true

View File

@@ -10,6 +10,6 @@ rustup target add wasm32-unknown-unknown
# For generating JS bindings:
# Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml
if ! cargo install --list | grep -q 'wasm-bindgen-cli v0.2.108'; then
cargo install --force --quiet wasm-bindgen-cli --version 0.2.108 --locked
if ! cargo install --list | grep -q 'wasm-bindgen-cli v0.2.126'; then
cargo install --force --quiet wasm-bindgen-cli --version 0.2.126 --locked
fi

View File

@@ -2,6 +2,7 @@ use std::sync::Arc;
use egui::ScrollArea;
use egui::accesskit::Role;
#[cfg(debug_assertions)]
use egui::epaint::Shape;
use egui::style::ScrollAnimation;
use egui::text::{LayoutJob, TextWrapping};
@@ -261,6 +262,7 @@ fn interact_on_ui_response_should_be_stable() {
assert_eq!(click_count, 10, "We missed some clicks!");
}
#[cfg(debug_assertions)]
fn has_red_warning_rect(output: &egui::FullOutput) -> bool {
output.shapes.iter().any(|clipped| {
matches!(
@@ -276,10 +278,13 @@ fn has_red_warning_rect(output: &egui::FullOutput) -> bool {
/// between frames because the label (and thus the Id salt) changes on hover.
/// The `warn_if_rect_changes_id` debug check should catch this.
#[test]
#[cfg(debug_assertions)]
fn warn_if_rect_changes_id() {
let button_rect = egui::Rect::from_min_size(egui::pos2(10.0, 10.0), egui::vec2(100.0, 30.0));
let mut harness = Harness::builder().with_size((200.0, 50.0)).build_ui(|ui| {
ui.global_style_mut(|style| style.debug.warn_if_rect_changes_id = true);
// Simulate a buggy widget whose Id depends on its label text,
// and the label changes on hover:
let is_hovered = ui.rect_contains_pointer(button_rect);
@@ -309,6 +314,7 @@ fn warn_if_rect_changes_id() {
/// all child widget ids shift too. This should NOT trigger `warn_if_rect_changes_id` because the
/// `parent_id` also changed — it's a cascading id shift, not a widget bug.
#[test]
#[cfg(debug_assertions)]
fn warn_if_rect_changes_id_false_positive_parent_shift() {
use std::cell::Cell;
@@ -316,6 +322,8 @@ fn warn_if_rect_changes_id_false_positive_parent_shift() {
let button_rect = egui::Rect::from_min_size(egui::pos2(10.0, 10.0), egui::vec2(100.0, 30.0));
let mut harness = Harness::builder().with_size((200.0, 100.0)).build_ui(|ui| {
ui.global_style_mut(|style| style.debug.warn_if_rect_changes_id = true);
// push_id with a changing value causes the child Ui's id to shift,
// which in turn shifts all widget ids inside it.
ui.push_id(counter.get(), |ui| {

View File

@@ -3,7 +3,7 @@ name = "test_background_logic"
version = "0.1.0"
license = "MIT OR Apache-2.0"
edition = "2024"
rust-version = "1.92"
rust-version = "1.95"
publish = false
[lints]

View File

@@ -3,7 +3,7 @@ name = "test_egui_extras_compilation"
version = "0.1.0"
license = "MIT OR Apache-2.0"
edition = "2024"
rust-version = "1.92"
rust-version = "1.95"
publish = false
[lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2024"
rust-version = "1.92"
rust-version = "1.95"
publish = false
[lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2024"
rust-version = "1.92"
rust-version = "1.95"
publish = false
[lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Antoine Beyeler <abeyeler@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2024"
rust-version = "1.92"
rust-version = "1.95"
publish = false
[lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["konkitoman"]
license = "MIT OR Apache-2.0"
edition = "2024"
rust-version = "1.92"
rust-version = "1.95"
publish = false
[lints]

View File

@@ -27,7 +27,7 @@ pub fn ask_to_run(mut cmd: Command, ask: bool, reason: &str) -> Result<(), DynEr
let is_ci = env::var_os("CI").is_some() || env::var_os("TF_BUILD").is_some();
if ask && !is_ci {
let mut buf = String::new();
print!("The script is going to run: \n\n`{cmd:?}`\n\n To {reason}.\nProceed? [Y/n] ",);
print!("The script is going to run: \n\n`{cmd:?}`\n\n To {reason}.\nProceed? [Y/n] ");
io::stdout().flush().unwrap();
io::stdin().read_line(&mut buf).unwrap();
match buf.trim().to_lowercase().as_ref() {