1
0
mirror of https://github.com/emilk/egui.git synced 2026-09-02 14:50:03 -04:00

Merge branch 'main' into update-wgpu-28

# Conflicts:
#	Cargo.lock
This commit is contained in:
lucasmerlin
2026-03-16 10:36:41 +01:00
171 changed files with 1783 additions and 1076 deletions

View File

@@ -9,6 +9,7 @@ isse = "isse" # part of @IsseW username
tye = "tye" # part of @tye-exe username tye = "tye" # part of @tye-exe username
ro = "ro" # read-only, also part of the username @Phen-Ro ro = "ro" # read-only, also part of the username @Phen-Ro
typ = "typ" # Often used because `type` is a keyword in Rust typ = "typ" # Often used because `type` is a keyword in Rust
wdth = "wdth" # The `wdth` tag is used in variable fonts
# I mistype these so often # I mistype these so often
tesalator = "tessellator" tesalator = "tessellator"
@@ -20,6 +21,8 @@ teselation = "tessellation"
tessalation = "tessellation" tessalation = "tessellation"
tesselation = "tessellation" tesselation = "tessellation"
# For consistency
postfix = "suffix"
# Use the more common spelling # Use the more common spelling
adaptor = "adapter" adaptor = "adapter"

File diff suppressed because it is too large Load Diff

View File

@@ -68,9 +68,9 @@ egui_glow = { version = "0.33.3", path = "crates/egui_glow", default-features =
egui_kittest = { version = "0.33.3", path = "crates/egui_kittest", default-features = false } egui_kittest = { version = "0.33.3", path = "crates/egui_kittest", default-features = false }
eframe = { version = "0.33.3", path = "crates/eframe", default-features = false } eframe = { version = "0.33.3", path = "crates/eframe", default-features = false }
accesskit = "0.21.1" accesskit = "0.24.0"
accesskit_consumer = "0.30.1" accesskit_consumer = "0.35.0"
accesskit_winit = "0.29.1" accesskit_winit = "0.32.0"
ahash = { version = "0.8.12", default-features = false, features = [ 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 "no-rng", # we don't need DOS-protection, so we let users opt-in to it instead
"std", "std",
@@ -84,12 +84,13 @@ chrono = { version = "0.4.42", default-features = false }
cint = "0.3.1" cint = "0.3.1"
color-hex = "0.2.0" color-hex = "0.2.0"
criterion = { version = "0.7.0", default-features = false } criterion = { version = "0.7.0", default-features = false }
dify = { version = "0.7.4", default-features = false } dify = { version = "0.8", default-features = false }
directories = "6.0.0" directories = "6.0.0"
document-features = "0.2.11" document-features = "0.2.11"
ehttp = { version = "0.6.0", default-features = false } ehttp = { version = "0.6.0", default-features = false }
enum-map = "2.7.3" enum-map = "2.7.3"
env_logger = { version = "0.11.8", default-features = false } env_logger = { version = "0.11.8", default-features = false }
font-types = { version = "0.11.0", default-features = false, features = ["std"] }
glow = "0.16.0" glow = "0.16.0"
glutin = { version = "0.32.3", default-features = false } glutin = { version = "0.32.3", default-features = false }
glutin-winit = { version = "0.5.0", default-features = false } glutin-winit = { version = "0.5.0", default-features = false }
@@ -119,24 +120,24 @@ rand = "0.9.2"
raw-window-handle = "0.6.2" raw-window-handle = "0.6.2"
rayon = "1.11.0" rayon = "1.11.0"
resvg = { version = "0.45.1", default-features = false } resvg = { version = "0.45.1", default-features = false }
rfd = "0.15.4" rfd = "0.17.2"
ron = "0.11.0" ron = "0.11.0"
self_cell = "1.2.1" self_cell = "1.2.1"
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
similar-asserts = "1.7.0" similar-asserts = "1.7.0"
skrifa = { version = "0.37.0", default-features = false, features = ["std", "autohint_shaping"] } skrifa = { version = "0.40.0", default-features = false, features = ["std", "autohint_shaping"] }
smallvec = "1.15.1" smallvec = "1.15.1"
smithay-clipboard = "0.7.2" smithay-clipboard = "0.7.2"
static_assertions = "1.1.0" static_assertions = "1.1.0"
syntect = { version = "5.3.0", default-features = false } syntect = { version = "5.3.0", default-features = false }
tempfile = "3.23.0" tempfile = "3.23.0"
thiserror = "2.0.17" thiserror = "2.0.17"
tokio = "1.47.1" tokio = "1.49"
toml = "0.8" toml = {version = "1", default-features = false }
type-map = "0.5.1" type-map = "0.5.1"
unicode_names2 = { version = "2.0.0", default-features = false } unicode_names2 = { version = "2.0.0", default-features = false }
unicode-segmentation = "1.12.0" unicode-segmentation = "1.12.0"
vello_cpu = { version = "0.0.4", default-features = false, features = ["std"] } vello_cpu = { version = "0.0.6", default-features = false, features = ["std", "u8_pipeline", "f32_pipeline"] }
wasm-bindgen = "0.2.100" # Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml wasm-bindgen = "0.2.100" # Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml
wasm-bindgen-futures = "0.4.0" wasm-bindgen-futures = "0.4.0"
wayland-cursor = { version = "0.31.11", default-features = false } wayland-cursor = { version = "0.31.11", default-features = false }
@@ -147,6 +148,8 @@ wgpu = { version = "28.0.0", default-features = false, features = ["std"] }
windows-sys = "0.61.2" windows-sys = "0.61.2"
winit = { version = "0.30.12", default-features = false } winit = { version = "0.30.12", default-features = false }
[patch.crates-io]
kittest = { git = "https://github.com/rerun-io/kittest", branch = "main" }
[workspace.lints.rust] [workspace.lints.rust]
unsafe_code = "deny" unsafe_code = "deny"

View File

@@ -72,7 +72,8 @@ pub struct CreationContext<'s> {
/// The `get_proc_address` wrapper of underlying GL context /// The `get_proc_address` wrapper of underlying GL context
#[cfg(feature = "glow")] #[cfg(feature = "glow")]
pub get_proc_address: Option<&'s dyn Fn(&std::ffi::CStr) -> *const std::ffi::c_void>, pub get_proc_address:
Option<std::sync::Arc<dyn Fn(&std::ffi::CStr) -> *const std::ffi::c_void + Send + Sync>>,
/// The underlying WGPU render state. /// The underlying WGPU render state.
/// ///

View File

@@ -265,6 +265,7 @@ impl EpiIntegration {
app: &mut dyn epi::App, app: &mut dyn epi::App,
viewport_ui_cb: Option<&DeferredViewportUiCallback>, viewport_ui_cb: Option<&DeferredViewportUiCallback>,
mut raw_input: egui::RawInput, mut raw_input: egui::RawInput,
is_visible: bool,
) -> egui::FullOutput { ) -> egui::FullOutput {
raw_input.time = Some(self.beginning.elapsed().as_secs_f64()); raw_input.time = Some(self.beginning.elapsed().as_secs_f64());
@@ -275,23 +276,27 @@ impl EpiIntegration {
let full_output = self.egui_ctx.run_ui(raw_input, |ui| { let full_output = self.egui_ctx.run_ui(raw_input, |ui| {
if let Some(viewport_ui_cb) = viewport_ui_cb { if let Some(viewport_ui_cb) = viewport_ui_cb {
// Child viewport // Child viewport
profiling::scope!("viewport_callback"); if is_visible {
viewport_ui_cb(ui); profiling::scope!("viewport_callback");
viewport_ui_cb(ui);
}
} else { } else {
{ {
profiling::scope!("App::logic"); profiling::scope!("App::logic");
app.logic(ui.ctx(), &mut self.frame); app.logic(ui.ctx(), &mut self.frame);
} }
{ if is_visible {
profiling::scope!("App::update"); {
#[expect(deprecated)] profiling::scope!("App::update");
app.update(ui.ctx(), &mut self.frame); #[expect(deprecated)]
} app.update(ui.ctx(), &mut self.frame);
}
{ {
profiling::scope!("App::ui"); profiling::scope!("App::ui");
app.ui(ui, &mut self.frame); app.ui(ui, &mut self.frame);
}
} }
} }
}); });

View File

@@ -294,14 +294,15 @@ impl<'app> GlowWinitApp<'app> {
// Use latest raw_window_handle for eframe compatibility // Use latest raw_window_handle for eframe compatibility
use raw_window_handle::{HasDisplayHandle as _, HasWindowHandle as _}; use raw_window_handle::{HasDisplayHandle as _, HasWindowHandle as _};
let get_proc_address = |addr: &_| glutin.get_proc_address(addr); let gl_config = glutin.gl_config.clone();
let get_proc_address = move |addr: &_| gl_config.display().get_proc_address(addr);
let window = glutin.window(ViewportId::ROOT); let window = glutin.window(ViewportId::ROOT);
let cc = CreationContext { let cc = CreationContext {
egui_ctx: integration.egui_ctx.clone(), egui_ctx: integration.egui_ctx.clone(),
integration_info: integration.frame.info().clone(), integration_info: integration.frame.info().clone(),
storage: integration.frame.storage(), storage: integration.frame.storage(),
gl: Some(gl), gl: Some(gl),
get_proc_address: Some(&get_proc_address), get_proc_address: Some(Arc::new(get_proc_address)),
#[cfg(feature = "wgpu_no_default_features")] #[cfg(feature = "wgpu_no_default_features")]
wgpu_render_state: None, wgpu_render_state: None,
raw_display_handle: window.display_handle().map(|h| h.as_raw()), raw_display_handle: window.display_handle().map(|h| h.as_raw()),
@@ -447,12 +448,21 @@ impl WinitApp for GlowWinitApp<'_> {
if let Some(viewport) = glutin if let Some(viewport) = glutin
.focused_viewport .focused_viewport
.and_then(|viewport| glutin.viewports.get_mut(&viewport)) .and_then(|viewport| glutin.viewports.get_mut(&viewport))
&& let Some(window) = viewport.window.as_ref()
{ {
if let Some(egui_winit) = viewport.egui_winit.as_mut() { if !window.has_focus()
egui_winit.on_mouse_motion(delta); && !viewport
.egui_winit
.as_ref()
.map(|state| state.is_any_pointer_button_down())
.unwrap_or(false)
{
return Ok(EventResult::Wait);
} }
if let Some(window) = viewport.window.as_ref() { if let Some(egui_winit) = viewport.egui_winit.as_mut()
&& egui_winit.on_mouse_motion(delta)
{
return Ok(EventResult::RepaintNext(window.id())); return Ok(EventResult::RepaintNext(window.id()));
} }
} }
@@ -535,7 +545,7 @@ impl GlowWinitRunning<'_> {
} }
} }
let (raw_input, viewport_ui_cb) = { let (raw_input, viewport_ui_cb, is_visible) = {
let mut glutin = self.glutin.borrow_mut(); let mut glutin = self.glutin.borrow_mut();
let egui_ctx = glutin.egui_ctx.clone(); let egui_ctx = glutin.egui_ctx.clone();
let Some(viewport) = glutin.viewports.get_mut(&viewport_id) else { let Some(viewport) = glutin.viewports.get_mut(&viewport_id) else {
@@ -546,6 +556,8 @@ impl GlowWinitRunning<'_> {
}; };
egui_winit::update_viewport_info(&mut viewport.info, &egui_ctx, window, false); egui_winit::update_viewport_info(&mut viewport.info, &egui_ctx, window, false);
let is_visible = viewport.info.visible().unwrap_or(true);
let Some(egui_winit) = viewport.egui_winit.as_mut() else { let Some(egui_winit) = viewport.egui_winit.as_mut() else {
return Ok(EventResult::Wait); return Ok(EventResult::Wait);
}; };
@@ -561,7 +573,7 @@ impl GlowWinitRunning<'_> {
.map(|(id, viewport)| (*id, viewport.info.clone())) .map(|(id, viewport)| (*id, viewport.info.clone()))
.collect(); .collect();
(raw_input, viewport_ui_cb) (raw_input, viewport_ui_cb, is_visible)
}; };
// HACK: In order to get the right clear_color, the system theme needs to be set, which // HACK: In order to get the right clear_color, the system theme needs to be set, which
@@ -577,7 +589,7 @@ impl GlowWinitRunning<'_> {
let has_many_viewports = self.glutin.borrow().viewports.len() > 1; let has_many_viewports = self.glutin.borrow().viewports.len() > 1;
let clear_before_update = !has_many_viewports; // HACK: for some reason, an early clear doesn't "take" on Mac with multiple viewports. let clear_before_update = !has_many_viewports; // HACK: for some reason, an early clear doesn't "take" on Mac with multiple viewports.
if clear_before_update { if is_visible && clear_before_update {
// clear before we call update, so users can paint between clear-color and egui windows: // clear before we call update, so users can paint between clear-color and egui windows:
let mut glutin = self.glutin.borrow_mut(); let mut glutin = self.glutin.borrow_mut();
@@ -612,9 +624,12 @@ impl GlowWinitRunning<'_> {
// The update function, which could call immediate viewports, // The update function, which could call immediate viewports,
// so make sure we don't hold any locks here required by the immediate viewports rendeer. // so make sure we don't hold any locks here required by the immediate viewports rendeer.
let full_output = let full_output = self.integration.update(
self.integration self.app.as_mut(),
.update(self.app.as_mut(), viewport_ui_cb.as_deref(), raw_input); viewport_ui_cb.as_deref(),
raw_input,
is_visible,
);
// ------------------------------------------------------------ // ------------------------------------------------------------
@@ -657,85 +672,87 @@ impl GlowWinitRunning<'_> {
egui_winit.handle_platform_output(&window, platform_output); egui_winit.handle_platform_output(&window, platform_output);
let clipped_primitives = integration.egui_ctx.tessellate(shapes, pixels_per_point); if is_visible {
let clipped_primitives = integration.egui_ctx.tessellate(shapes, pixels_per_point);
{ {
// We may need to switch contexts again, because of immediate viewports: // We may need to switch contexts again, because of immediate viewports:
frame_timer.pause(); frame_timer.pause();
change_gl_context(current_gl_context, not_current_gl_context, gl_surface); change_gl_context(current_gl_context, not_current_gl_context, gl_surface);
frame_timer.resume(); frame_timer.resume();
} }
let screen_size_in_pixels: [u32; 2] = window.inner_size().into(); let screen_size_in_pixels: [u32; 2] = window.inner_size().into();
if !clear_before_update { if !clear_before_update {
painter.clear(screen_size_in_pixels, clear_color); painter.clear(screen_size_in_pixels, clear_color);
} }
painter.paint_and_update_textures( painter.paint_and_update_textures(
screen_size_in_pixels, screen_size_in_pixels,
pixels_per_point, pixels_per_point,
&clipped_primitives, &clipped_primitives,
&textures_delta, &textures_delta,
); );
{ {
for action in viewport.actions_requested.drain(..) { for action in viewport.actions_requested.drain(..) {
match action { match action {
ActionRequested::Screenshot(user_data) => { ActionRequested::Screenshot(user_data) => {
let screenshot = painter.read_screen_rgba(screen_size_in_pixels); let screenshot = painter.read_screen_rgba(screen_size_in_pixels);
egui_winit egui_winit
.egui_input_mut() .egui_input_mut()
.events .events
.push(egui::Event::Screenshot { .push(egui::Event::Screenshot {
viewport_id, viewport_id,
user_data, user_data,
image: screenshot.into(), image: screenshot.into(),
}); });
} }
ActionRequested::Cut => { ActionRequested::Cut => {
egui_winit.egui_input_mut().events.push(egui::Event::Cut); egui_winit.egui_input_mut().events.push(egui::Event::Cut);
} }
ActionRequested::Copy => { ActionRequested::Copy => {
egui_winit.egui_input_mut().events.push(egui::Event::Copy); egui_winit.egui_input_mut().events.push(egui::Event::Copy);
} }
ActionRequested::Paste => { ActionRequested::Paste => {
if let Some(contents) = egui_winit.clipboard_text() { if let Some(contents) = egui_winit.clipboard_text() {
let contents = contents.replace("\r\n", "\n"); let contents = contents.replace("\r\n", "\n");
if !contents.is_empty() { if !contents.is_empty() {
egui_winit egui_winit
.egui_input_mut() .egui_input_mut()
.events .events
.push(egui::Event::Paste(contents)); .push(egui::Event::Paste(contents));
}
} }
} }
} }
} }
integration.post_rendering(&window);
} }
integration.post_rendering(&window); {
} // vsync - don't count as frame-time:
frame_timer.pause();
profiling::scope!("swap_buffers");
let context = current_gl_context.as_ref().ok_or_else(|| {
egui_glow::PainterError::from(
"failed to get current context to swap buffers".to_owned(),
)
})?;
{ gl_surface.swap_buffers(context)?;
// vsync - don't count as frame-time: frame_timer.resume();
frame_timer.pause(); }
profiling::scope!("swap_buffers");
let context = current_gl_context.as_ref().ok_or_else(|| {
egui_glow::PainterError::from(
"failed to get current context to swap buffers".to_owned(),
)
})?;
gl_surface.swap_buffers(context)?; // give it time to settle:
frame_timer.resume(); #[cfg(feature = "__screenshot")]
} if integration.egui_ctx.cumulative_pass_nr() == 2
&& let Ok(path) = std::env::var("EFRAME_SCREENSHOT_TO")
// give it time to settle: {
#[cfg(feature = "__screenshot")] save_screenshot_and_exit(&path, &painter, screen_size_in_pixels);
if integration.egui_ctx.cumulative_pass_nr() == 2 }
&& let Ok(path) = std::env::var("EFRAME_SCREENSHOT_TO")
{
save_screenshot_and_exit(&path, &painter, screen_size_in_pixels);
} }
glutin.handle_viewport_output(event_loop, &integration.egui_ctx, &viewport_output); glutin.handle_viewport_output(event_loop, &integration.egui_ctx, &viewport_output);
@@ -812,6 +829,14 @@ impl GlowWinitRunning<'_> {
} }
} }
winit::event::WindowEvent::Occluded(is_occluded) => {
if let Some(viewport_id) = viewport_id
&& let Some(viewport) = glutin.viewports.get_mut(&viewport_id)
{
viewport.info.occluded = Some(*is_occluded);
}
}
winit::event::WindowEvent::CloseRequested => { winit::event::WindowEvent::CloseRequested => {
if viewport_id == Some(ViewportId::ROOT) && self.integration.should_close() { if viewport_id == Some(ViewportId::ROOT) && self.integration.should_close() {
log::debug!( log::debug!(

View File

@@ -454,12 +454,21 @@ impl WinitApp for WgpuWinitApp<'_> {
if let Some(viewport) = shared if let Some(viewport) = shared
.focused_viewport .focused_viewport
.and_then(|viewport| shared.viewports.get_mut(&viewport)) .and_then(|viewport| shared.viewports.get_mut(&viewport))
&& let Some(window) = viewport.window.as_ref()
{ {
if let Some(egui_winit) = viewport.egui_winit.as_mut() { if !window.has_focus()
egui_winit.on_mouse_motion(delta); && !viewport
.egui_winit
.as_ref()
.map(|state| state.is_any_pointer_button_down())
.unwrap_or(false)
{
return Ok(EventResult::Wait);
} }
if let Some(window) = viewport.window.as_ref() { if let Some(egui_winit) = viewport.egui_winit.as_mut()
&& egui_winit.on_mouse_motion(delta)
{
return Ok(EventResult::RepaintNext(window.id())); return Ok(EventResult::RepaintNext(window.id()));
} }
} }
@@ -564,7 +573,7 @@ impl WgpuWinitRunning<'_> {
let mut frame_timer = crate::stopwatch::Stopwatch::new(); let mut frame_timer = crate::stopwatch::Stopwatch::new();
frame_timer.start(); frame_timer.start();
let (viewport_ui_cb, raw_input) = { let (viewport_ui_cb, raw_input, is_visible) = {
profiling::scope!("Prepare"); profiling::scope!("Prepare");
let mut shared_lock = shared.borrow_mut(); let mut shared_lock = shared.borrow_mut();
@@ -608,6 +617,8 @@ impl WgpuWinitRunning<'_> {
}; };
egui_winit::update_viewport_info(info, &integration.egui_ctx, window, false); egui_winit::update_viewport_info(info, &integration.egui_ctx, window, false);
let is_visible = viewport.info.visible().unwrap_or(true);
{ {
profiling::scope!("set_window"); profiling::scope!("set_window");
pollster::block_on(painter.set_window(viewport_id, Some(Arc::clone(window))))?; pollster::block_on(painter.set_window(viewport_id, Some(Arc::clone(window))))?;
@@ -628,14 +639,19 @@ impl WgpuWinitRunning<'_> {
painter.handle_screenshots(&mut raw_input.events); painter.handle_screenshots(&mut raw_input.events);
(viewport_ui_cb, raw_input) (viewport_ui_cb, raw_input, is_visible)
}; };
// ------------------------------------------------------------ // ------------------------------------------------------------
// Runs the update, which could call immediate viewports, // Runs the update, which could call immediate viewports,
// so make sure we hold no locks here! // so make sure we hold no locks here!
let full_output = integration.update(app.as_mut(), viewport_ui_cb.as_deref(), raw_input); let full_output = integration.update(
app.as_mut(),
viewport_ui_cb.as_deref(),
raw_input,
is_visible,
);
// ------------------------------------------------------------ // ------------------------------------------------------------
@@ -676,52 +692,58 @@ impl WgpuWinitRunning<'_> {
egui_winit.handle_platform_output(window, platform_output); egui_winit.handle_platform_output(window, platform_output);
let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point); let vsync_secs = if is_visible {
let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point);
let mut screenshot_commands = vec![]; let mut screenshot_commands = vec![];
viewport.actions_requested.retain(|cmd| { viewport.actions_requested.retain(|cmd| {
if let ActionRequested::Screenshot(info) = cmd { if let ActionRequested::Screenshot(info) = cmd {
screenshot_commands.push(info.clone()); screenshot_commands.push(info.clone());
false false
} else { } else {
true true
} }
}); });
let vsync_secs = painter.paint_and_update_textures( let vsync_secs = painter.paint_and_update_textures(
viewport_id, viewport_id,
pixels_per_point, pixels_per_point,
app.clear_color(&egui_ctx.global_style().visuals), app.clear_color(&egui_ctx.global_style().visuals),
&clipped_primitives, &clipped_primitives,
&textures_delta, &textures_delta,
screenshot_commands, screenshot_commands,
); );
for action in viewport.actions_requested.drain(..) { for action in viewport.actions_requested.drain(..) {
match action { match action {
ActionRequested::Screenshot { .. } => { ActionRequested::Screenshot { .. } => {
// already handled above // already handled above
} }
ActionRequested::Cut => { ActionRequested::Cut => {
egui_winit.egui_input_mut().events.push(egui::Event::Cut); egui_winit.egui_input_mut().events.push(egui::Event::Cut);
} }
ActionRequested::Copy => { ActionRequested::Copy => {
egui_winit.egui_input_mut().events.push(egui::Event::Copy); egui_winit.egui_input_mut().events.push(egui::Event::Copy);
} }
ActionRequested::Paste => { ActionRequested::Paste => {
if let Some(contents) = egui_winit.clipboard_text() { if let Some(contents) = egui_winit.clipboard_text() {
let contents = contents.replace("\r\n", "\n"); let contents = contents.replace("\r\n", "\n");
if !contents.is_empty() { if !contents.is_empty() {
egui_winit egui_winit
.egui_input_mut() .egui_input_mut()
.events .events
.push(egui::Event::Paste(contents)); .push(egui::Event::Paste(contents));
}
} }
} }
} }
} }
}
integration.post_rendering(window); integration.post_rendering(window);
vsync_secs
} else {
0.0
};
let active_viewports_ids: ViewportIdSet = viewport_output.keys().copied().collect(); let active_viewports_ids: ViewportIdSet = viewport_output.keys().copied().collect();
@@ -843,6 +865,14 @@ impl WgpuWinitRunning<'_> {
} }
} }
winit::event::WindowEvent::Occluded(is_occluded) => {
if let Some(viewport_id) = viewport_id
&& let Some(viewport) = shared.viewports.get_mut(&viewport_id)
{
viewport.info.occluded = Some(*is_occluded);
}
}
winit::event::WindowEvent::CloseRequested => { winit::event::WindowEvent::CloseRequested => {
if viewport_id == Some(ViewportId::ROOT) && integration.should_close() { if viewport_id == Some(ViewportId::ROOT) && integration.should_close() {
log::debug!( log::debug!(

View File

@@ -274,13 +274,21 @@ impl AppRunner {
self.app.raw_input_hook(&self.egui_ctx, &mut raw_input); self.app.raw_input_hook(&self.egui_ctx, &mut raw_input);
let is_visible = raw_input
.viewports
.get(&egui::ViewportId::ROOT)
.and_then(|v| v.visible())
.unwrap_or(true);
let full_output = self.egui_ctx.run_ui(raw_input, |ui| { let full_output = self.egui_ctx.run_ui(raw_input, |ui| {
self.app.logic(ui.ctx(), &mut self.frame); self.app.logic(ui.ctx(), &mut self.frame);
#[expect(deprecated)] if is_visible {
self.app.update(ui.ctx(), &mut self.frame); #[expect(deprecated)]
self.app.update(ui.ctx(), &mut self.frame);
self.app.ui(ui, &mut self.frame); self.app.ui(ui, &mut self.frame);
}
}); });
let egui::FullOutput { let egui::FullOutput {
platform_output, platform_output,
@@ -311,8 +319,10 @@ impl AppRunner {
} }
self.handle_platform_output(platform_output); self.handle_platform_output(platform_output);
self.textures_delta.append(textures_delta); if is_visible {
self.clipped_primitives = Some(self.egui_ctx.tessellate(shapes, pixels_per_point)); self.textures_delta.append(textures_delta);
self.clipped_primitives = Some(self.egui_ctx.tessellate(shapes, pixels_per_point));
}
} }
/// Paint the results of the last call to [`Self::logic`]. /// Paint the results of the last call to [`Self::logic`].

View File

@@ -31,11 +31,18 @@ impl WebInput {
time: Some(super::now_sec()), time: Some(super::now_sec()),
..self.raw.take() ..self.raw.take()
}; };
raw_input let viewport = raw_input
.viewports .viewports
.entry(egui::ViewportId::ROOT) .entry(egui::ViewportId::ROOT)
.or_default() .or_default();
.native_pixels_per_point = Some(super::native_pixels_per_point()); viewport.native_pixels_per_point = Some(super::native_pixels_per_point());
// A hidden browser tab is effectively occluded.
let hidden = web_sys::window()
.and_then(|w| w.document())
.is_some_and(|doc| doc.hidden());
viewport.occluded = Some(hidden);
raw_input raw_input
} }

View File

@@ -420,12 +420,40 @@ impl Painter {
) -> f32 { ) -> f32 {
profiling::function_scope!(); profiling::function_scope!();
/// Guard to ensure that commands are always submitted to the renderer queue
/// so that calls to [`write_buffer()`](https://docs.rs/wgpu/latest/wgpu/struct.Queue.html#method.write_buffer)
/// are completed even if we take a codepath which doesn't submit commands and avoids
/// internal buffers growing indefinitely.
///
/// This may happen, for example, if no output frame is resolved.
/// See <https://github.com/emilk/egui/pull/7928> for full context.
struct RendererQueueGuard<'q> {
queue: &'q wgpu::Queue,
commands_submitted: bool,
}
impl Drop for RendererQueueGuard<'_> {
fn drop(&mut self) {
// Only submit an empty command buffer array if no commands were
// explicitly submitted.
if !self.commands_submitted {
self.queue.submit([]);
}
}
}
let capture = !capture_data.is_empty(); let capture = !capture_data.is_empty();
let mut vsync_sec = 0.0; let mut vsync_sec = 0.0;
let Some(render_state) = self.render_state.as_mut() else { let Some(render_state) = self.render_state.as_mut() else {
return vsync_sec; return vsync_sec;
}; };
let mut render_queue_guard = RendererQueueGuard {
queue: &render_state.queue,
commands_submitted: false,
};
let Some(surface_state) = self.surfaces.get(&viewport_id) else { let Some(surface_state) = self.surfaces.get(&viewport_id) else {
return vsync_sec; return vsync_sec;
}; };
@@ -590,6 +618,9 @@ impl Painter {
vsync_sec += start.elapsed().as_secs_f32(); vsync_sec += start.elapsed().as_secs_f32();
}; };
// Ensure that the queue guard does not do unnecessary work when dropped
render_queue_guard.commands_submitted = true;
// Free textures marked for destruction **after** queue submit since they might still be used in the current frame. // Free textures marked for destruction **after** queue submit since they might still be used in the current frame.
// Calling `wgpu::Texture::destroy` on a texture that is still in use would invalidate the command buffer(s) it is used in. // Calling `wgpu::Texture::destroy` on a texture that is still in use would invalidate the command buffer(s) it is used in.
// However, once we called `wgpu::Queue::submit`, it is up for wgpu to determine how long the underlying gpu resource has to live. // However, once we called `wgpu::Queue::submit`, it is up for wgpu to determine how long the underlying gpu resource has to live.

View File

@@ -102,7 +102,7 @@ pub struct State {
has_sent_ime_enabled: bool, has_sent_ime_enabled: bool,
#[cfg(feature = "accesskit")] #[cfg(feature = "accesskit")]
accesskit: Option<accesskit_winit::Adapter>, pub accesskit: Option<accesskit_winit::Adapter>,
allow_ime: bool, allow_ime: bool,
ime_rect_px: Option<egui::Rect>, ime_rect_px: Option<egui::Rect>,
@@ -640,11 +640,27 @@ impl State {
self.has_sent_ime_enabled = false; self.has_sent_ime_enabled = false;
} }
pub fn on_mouse_motion(&mut self, delta: (f64, f64)) { /// Returns `true` if the event was sent to egui.
pub fn on_mouse_motion(&mut self, delta: (f64, f64)) -> bool {
if !self.is_pointer_in_window() && !self.any_pointer_button_down {
return false;
}
self.egui_input.events.push(egui::Event::MouseMoved(Vec2 { self.egui_input.events.push(egui::Event::MouseMoved(Vec2 {
x: delta.0 as f32, x: delta.0 as f32,
y: delta.1 as f32, y: delta.1 as f32,
})); }));
true
}
/// Returns `true` when the pointer is currently inside the window.
pub fn is_pointer_in_window(&self) -> bool {
self.pointer_pos_in_points.is_some()
}
/// Returns `true` if any pointer button is currently held down.
pub fn is_any_pointer_button_down(&self) -> bool {
self.any_pointer_button_down
} }
/// Call this when there is a new [`accesskit::ActionRequest`]. /// Call this when there is a new [`accesskit::ActionRequest`].

View File

@@ -1,5 +1,5 @@
use crate::{AtomKind, FontSelection, Id, SizedAtom, Ui}; use crate::{AtomKind, FontSelection, Id, IntoSizedArgs, IntoSizedResult, SizedAtom, Ui};
use emath::{NumExt as _, Vec2}; use emath::{Align2, NumExt as _, Vec2};
use epaint::text::TextWrapMode; use epaint::text::TextWrapMode;
/// A low-level ui building block. /// A low-level ui building block.
@@ -14,6 +14,9 @@ use epaint::text::TextWrapMode;
/// ``` /// ```
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Atom<'a> { pub struct Atom<'a> {
/// See [`crate::AtomExt::atom_id`]
pub id: Option<Id>,
/// See [`crate::AtomExt::atom_size`] /// See [`crate::AtomExt::atom_size`]
pub size: Option<Vec2>, pub size: Option<Vec2>,
@@ -26,17 +29,22 @@ pub struct Atom<'a> {
/// See [`crate::AtomExt::atom_shrink`] /// See [`crate::AtomExt::atom_shrink`]
pub shrink: bool, pub shrink: bool,
/// The atom type /// See [`crate::AtomExt::atom_align`]
pub align: Align2,
/// The atom type / content
pub kind: AtomKind<'a>, pub kind: AtomKind<'a>,
} }
impl Default for Atom<'_> { impl Default for Atom<'_> {
fn default() -> Self { fn default() -> Self {
Atom { Atom {
id: None,
size: None, size: None,
max_size: Vec2::INFINITY, max_size: Vec2::INFINITY,
grow: false, grow: false,
shrink: false, shrink: false,
align: Align2::CENTER_CENTER,
kind: AtomKind::Empty, kind: AtomKind::Empty,
} }
} }
@@ -54,11 +62,27 @@ impl<'a> Atom<'a> {
} }
} }
/// Create a [`AtomKind::Custom`] with a specific size. /// Create an [`AtomKind::Empty`] with a specific size.
///
/// Example:
/// ```
/// # use egui::{AtomExt, AtomKind, Atom, Button, Id, __run_test_ui};
/// # use emath::Vec2;
/// # __run_test_ui(|ui| {
/// let id = Id::new("my_button");
/// let response = Button::new(("Hi!", Atom::custom(id, Vec2::splat(18.0)))).atom_ui(ui);
///
/// let rect = response.rect(id);
/// if let Some(rect) = rect {
/// ui.place(rect, Button::new("⏵"));
/// }
/// # });
/// ```
pub fn custom(id: Id, size: impl Into<Vec2>) -> Self { pub fn custom(id: Id, size: impl Into<Vec2>) -> Self {
Atom { Atom {
size: Some(size.into()), size: Some(size.into()),
kind: AtomKind::Custom(id), kind: AtomKind::Empty,
id: Some(id),
..Default::default() ..Default::default()
} }
} }
@@ -82,19 +106,32 @@ impl<'a> Atom<'a> {
wrap_mode = Some(TextWrapMode::Truncate); wrap_mode = Some(TextWrapMode::Truncate);
} }
let (intrinsic, kind) = self let id = self.id;
.kind
.into_sized(ui, available_size, wrap_mode, fallback_font); let wrap_mode = wrap_mode.unwrap_or_else(|| ui.wrap_mode());
let IntoSizedResult {
intrinsic_size,
sized,
} = self.kind.into_sized(
ui,
IntoSizedArgs {
available_size,
wrap_mode,
fallback_font,
},
);
let size = self let size = self
.size .size
.map_or_else(|| kind.size(), |s| s.at_most(self.max_size)); .map_or_else(|| sized.size(), |s| s.at_most(self.max_size));
SizedAtom { SizedAtom {
id,
size, size,
intrinsic_size: intrinsic.at_least(self.size.unwrap_or_default()), intrinsic_size: intrinsic_size.at_least(self.size.unwrap_or_default()),
grow: self.grow, grow: self.grow,
kind, align: self.align,
kind: sized,
} }
} }
} }

View File

@@ -1,10 +1,16 @@
use crate::{Atom, FontSelection, Ui}; use crate::{Atom, FontSelection, Id, Ui};
use emath::Vec2; use emath::Vec2;
/// A trait for conveniently building [`Atom`]s. /// A trait for conveniently building [`Atom`]s.
/// ///
/// The functions are prefixed with `atom_` to avoid conflicts with e.g. [`crate::RichText::size`]. /// The functions are prefixed with `atom_` to avoid conflicts with e.g. [`crate::RichText::size`].
pub trait AtomExt<'a> { pub trait AtomExt<'a> {
/// Set the [`Id`] for custom rendering.
///
/// You can get the [`crate::Rect`] with the [`Id`] from [`crate::AtomLayoutResponse`] and use a
/// [`crate::Painter`] or [`Ui::place`] to add/draw some custom content.
fn atom_id(self, id: Id) -> Atom<'a>;
/// Set the atom to a fixed size. /// Set the atom to a fixed size.
/// ///
/// If [`Atom::grow`] is `true`, this will be the minimum width. /// If [`Atom::grow`] is `true`, this will be the minimum width.
@@ -63,12 +69,23 @@ pub trait AtomExt<'a> {
let height = ui.fonts_mut(|f| f.row_height(&font_id)); let height = ui.fonts_mut(|f| f.row_height(&font_id));
self.atom_max_height(height) self.atom_max_height(height)
} }
/// Sets the [`emath::Align2`] of a single atom within its available space.
///
/// Defaults to center-center.
fn atom_align(self, align: emath::Align2) -> Atom<'a>;
} }
impl<'a, T> AtomExt<'a> for T impl<'a, T> AtomExt<'a> for T
where where
T: Into<Atom<'a>> + Sized, T: Into<Atom<'a>> + Sized,
{ {
fn atom_id(self, id: Id) -> Atom<'a> {
let mut atom = self.into();
atom.id = Some(id);
atom
}
fn atom_size(self, size: Vec2) -> Atom<'a> { fn atom_size(self, size: Vec2) -> Atom<'a> {
let mut atom = self.into(); let mut atom = self.into();
atom.size = Some(size); atom.size = Some(size);
@@ -104,4 +121,10 @@ where
atom.max_size.y = max_height; atom.max_size.y = max_height;
atom atom
} }
fn atom_align(self, align: emath::Align2) -> Atom<'a> {
let mut atom = self.into();
atom.align = align;
atom
}
} }

View File

@@ -1,9 +1,28 @@
use crate::{FontSelection, Id, Image, ImageSource, SizedAtomKind, Ui, WidgetText}; use crate::{FontSelection, Image, ImageSource, SizedAtomKind, Ui, WidgetText};
use emath::Vec2; use emath::Vec2;
use epaint::text::TextWrapMode; use epaint::text::TextWrapMode;
use std::fmt::Debug;
/// Args passed when sizing an [`super::Atom`]
pub struct IntoSizedArgs {
pub available_size: Vec2,
pub wrap_mode: TextWrapMode,
pub fallback_font: FontSelection,
}
/// Result returned when sizing an [`super::Atom`]
pub struct IntoSizedResult<'a> {
pub intrinsic_size: Vec2,
pub sized: SizedAtomKind<'a>,
}
/// See [`AtomKind::Closure`]
// We need 'static in the result (or need to introduce another lifetime on the enum).
// Otherwise, a single 'static Atom would force the closure to be 'static.
pub type AtomClosure<'a> = Box<dyn FnOnce(&Ui, IntoSizedArgs) -> IntoSizedResult<'static> + 'a>;
/// The different kinds of [`crate::Atom`]s. /// The different kinds of [`crate::Atom`]s.
#[derive(Clone, Default, Debug)] #[derive(Default)]
pub enum AtomKind<'a> { pub enum AtomKind<'a> {
/// Empty, that can be used with [`crate::AtomExt::atom_grow`] to reserve space. /// Empty, that can be used with [`crate::AtomExt::atom_grow`] to reserve space.
#[default] #[default]
@@ -38,37 +57,57 @@ pub enum AtomKind<'a> {
/// default font height, which is convenient for icons. /// default font height, which is convenient for icons.
Image(Image<'a>), Image(Image<'a>),
/// For custom rendering. /// A custom closure that produces a sized atom.
/// ///
/// You can get the [`crate::Rect`] with the [`Id`] from [`crate::AtomLayoutResponse`] and use a /// The vec2 passed in is the available size to this atom. The returned vec2 should be the
/// [`crate::Painter`] or [`Ui::place`] to add/draw some custom content. /// preferred / intrinsic size.
/// ///
/// Example: /// Note: This api is experimental, expect breaking changes here.
/// ``` /// When cloning, this will be cloned as [`AtomKind::Empty`].
/// # use egui::{AtomExt, AtomKind, Atom, Button, Id, __run_test_ui}; Closure(AtomClosure<'a>),
/// # use emath::Vec2; }
/// # __run_test_ui(|ui| {
/// let id = Id::new("my_button"); impl Clone for AtomKind<'_> {
/// let response = Button::new(("Hi!", Atom::custom(id, Vec2::splat(18.0)))).atom_ui(ui); fn clone(&self) -> Self {
/// match self {
/// let rect = response.rect(id); AtomKind::Empty => AtomKind::Empty,
/// if let Some(rect) = rect { AtomKind::Text(text) => AtomKind::Text(text.clone()),
/// ui.place(rect, Button::new("⏵")); AtomKind::Image(image) => AtomKind::Image(image.clone()),
/// } AtomKind::Closure(_) => {
/// # }); log::warn!("Cannot clone atom closures");
/// ``` AtomKind::Empty
Custom(Id), }
}
}
}
impl Debug for AtomKind<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AtomKind::Empty => write!(f, "AtomKind::Empty"),
AtomKind::Text(text) => write!(f, "AtomKind::Text({text:?})"),
AtomKind::Image(image) => write!(f, "AtomKind::Image({image:?})"),
AtomKind::Closure(_) => write!(f, "AtomKind::Closure(<closure>)"),
}
}
} }
impl<'a> AtomKind<'a> { impl<'a> AtomKind<'a> {
/// See [`Self::Text`]
pub fn text(text: impl Into<WidgetText>) -> Self { pub fn text(text: impl Into<WidgetText>) -> Self {
AtomKind::Text(text.into()) AtomKind::Text(text.into())
} }
/// See [`Self::Image`]
pub fn image(image: impl Into<Image<'a>>) -> Self { pub fn image(image: impl Into<Image<'a>>) -> Self {
AtomKind::Image(image.into()) AtomKind::Image(image.into())
} }
/// See [`Self::Closure`]
pub fn closure(func: impl FnOnce(&Ui, IntoSizedArgs) -> IntoSizedResult<'static> + 'a) -> Self {
AtomKind::Closure(Box::new(func))
}
/// Turn this [`AtomKind`] into a [`SizedAtomKind`]. /// Turn this [`AtomKind`] into a [`SizedAtomKind`].
/// ///
/// This converts [`WidgetText`] into [`crate::Galley`] and tries to load and size [`Image`]. /// This converts [`WidgetText`] into [`crate::Galley`] and tries to load and size [`Image`].
@@ -76,23 +115,40 @@ impl<'a> AtomKind<'a> {
pub fn into_sized( pub fn into_sized(
self, self,
ui: &Ui, ui: &Ui,
available_size: Vec2, IntoSizedArgs {
wrap_mode: Option<TextWrapMode>, available_size,
fallback_font: FontSelection, wrap_mode,
) -> (Vec2, SizedAtomKind<'a>) { fallback_font,
}: IntoSizedArgs,
) -> IntoSizedResult<'a> {
match self { match self {
AtomKind::Text(text) => { AtomKind::Text(text) => {
let wrap_mode = wrap_mode.unwrap_or_else(|| ui.wrap_mode());
let galley = text.into_galley(ui, Some(wrap_mode), available_size.x, fallback_font); let galley = text.into_galley(ui, Some(wrap_mode), available_size.x, fallback_font);
(galley.intrinsic_size(), SizedAtomKind::Text(galley)) IntoSizedResult {
intrinsic_size: galley.intrinsic_size(),
sized: SizedAtomKind::Text(galley),
}
} }
AtomKind::Image(image) => { AtomKind::Image(image) => {
let size = image.load_and_calc_size(ui, available_size); let size = image.load_and_calc_size(ui, available_size);
let size = size.unwrap_or(Vec2::ZERO); let size = size.unwrap_or(Vec2::ZERO);
(size, SizedAtomKind::Image(image, size)) IntoSizedResult {
intrinsic_size: size,
sized: SizedAtomKind::Image { image, size },
}
} }
AtomKind::Custom(id) => (Vec2::ZERO, SizedAtomKind::Custom(id)), AtomKind::Empty => IntoSizedResult {
AtomKind::Empty => (Vec2::ZERO, SizedAtomKind::Empty), intrinsic_size: Vec2::ZERO,
sized: SizedAtomKind::Empty { size: None },
},
AtomKind::Closure(func) => func(
ui,
IntoSizedArgs {
available_size,
wrap_mode,
fallback_font,
},
),
} }
} }
} }

View File

@@ -38,6 +38,7 @@ pub struct AtomLayout<'a> {
fallback_text_color: Option<Color32>, fallback_text_color: Option<Color32>,
fallback_font: Option<FontSelection>, fallback_font: Option<FontSelection>,
min_size: Vec2, min_size: Vec2,
max_size: Vec2,
wrap_mode: Option<TextWrapMode>, wrap_mode: Option<TextWrapMode>,
align2: Option<Align2>, align2: Option<Align2>,
} }
@@ -59,6 +60,7 @@ impl<'a> AtomLayout<'a> {
fallback_text_color: None, fallback_text_color: None,
fallback_font: None, fallback_font: None,
min_size: Vec2::ZERO, min_size: Vec2::ZERO,
max_size: Vec2::INFINITY,
wrap_mode: None, wrap_mode: None,
align2: None, align2: None,
} }
@@ -113,6 +115,33 @@ impl<'a> AtomLayout<'a> {
self self
} }
/// Set the maximum size of the Widget.
///
/// By default, the size is limited by the available size in the [`Ui`].
#[inline]
pub fn max_size(mut self, size: Vec2) -> Self {
self.max_size = size;
self
}
/// Set the maximum width of the Widget.
///
/// By default, the width is limited by the available width in the [`Ui`].
#[inline]
pub fn max_width(mut self, width: f32) -> Self {
self.max_size.x = width;
self
}
/// Set the maximum height of the Widget.
///
/// By default, the height is limited by the available height in the [`Ui`].
#[inline]
pub fn max_height(mut self, height: f32) -> Self {
self.max_size.y = height;
self
}
/// Set the [`Id`] used to allocate a [`Response`]. /// Set the [`Id`] used to allocate a [`Response`].
#[inline] #[inline]
pub fn id(mut self, id: Id) -> Self { pub fn id(mut self, id: Id) -> Self {
@@ -161,6 +190,7 @@ impl<'a> AtomLayout<'a> {
sense, sense,
fallback_text_color, fallback_text_color,
min_size, min_size,
mut max_size,
wrap_mode, wrap_mode,
align2, align2,
fallback_font, fallback_font,
@@ -190,8 +220,16 @@ impl<'a> AtomLayout<'a> {
fallback_text_color.unwrap_or_else(|| ui.style().visuals.text_color()); fallback_text_color.unwrap_or_else(|| ui.style().visuals.text_color());
let gap = gap.unwrap_or_else(|| ui.spacing().icon_spacing); let gap = gap.unwrap_or_else(|| ui.spacing().icon_spacing);
// max_size has no effect in justified layouts. If we'd limit the available size here,
// the content would be sized differently than the frame which would look weird.
if ui.layout().horizontal_justify() {
max_size.x = f32::INFINITY;
}
let available_size = ui.available_size().at_most(max_size);
// The size available for the content // The size available for the content
let available_inner_size = ui.available_size() - frame.total_margin().sum(); let available_inner_size = available_size - frame.total_margin().sum();
let mut desired_width = 0.0; let mut desired_width = 0.0;
@@ -321,7 +359,7 @@ impl<'atom> AllocatedAtomLayout<'atom> {
pub fn iter_images(&self) -> impl Iterator<Item = &Image<'atom>> { pub fn iter_images(&self) -> impl Iterator<Item = &Image<'atom>> {
self.iter_kinds().filter_map(|kind| { self.iter_kinds().filter_map(|kind| {
if let SizedAtomKind::Image(image, _) = kind { if let SizedAtomKind::Image { image, size: _ } = kind {
Some(image) Some(image)
} else { } else {
None None
@@ -331,7 +369,7 @@ impl<'atom> AllocatedAtomLayout<'atom> {
pub fn iter_images_mut(&mut self) -> impl Iterator<Item = &mut Image<'atom>> { pub fn iter_images_mut(&mut self) -> impl Iterator<Item = &mut Image<'atom>> {
self.iter_kinds_mut().filter_map(|kind| { self.iter_kinds_mut().filter_map(|kind| {
if let SizedAtomKind::Image(image, _) = kind { if let SizedAtomKind::Image { image, size: _ } = kind {
Some(image) Some(image)
} else { } else {
None None
@@ -373,8 +411,11 @@ impl<'atom> AllocatedAtomLayout<'atom> {
F: FnMut(Image<'atom>) -> Image<'atom>, F: FnMut(Image<'atom>) -> Image<'atom>,
{ {
self.map_kind(|kind| { self.map_kind(|kind| {
if let SizedAtomKind::Image(image, size) = kind { if let SizedAtomKind::Image { image, size } = kind {
SizedAtomKind::Image(f(image), size) SizedAtomKind::Image {
image: f(image),
size,
}
} else { } else {
kind kind
} }
@@ -422,25 +463,24 @@ impl<'atom> AllocatedAtomLayout<'atom> {
.with_min_x(cursor) .with_min_x(cursor)
.with_max_x(cursor + size.x + growth); .with_max_x(cursor + size.x + growth);
cursor = frame.right() + gap; cursor = frame.right() + gap;
let rect = sized.align.align_size_within_rect(size, frame);
let align = Align2::CENTER_CENTER; if let Some(id) = sized.id {
let rect = align.align_size_within_rect(size, frame); debug_assert!(
!response.custom_rects.iter().any(|(i, _)| *i == id),
"Duplicate custom id"
);
response.custom_rects.push((id, rect));
}
match sized.kind { match sized.kind {
SizedAtomKind::Text(galley) => { SizedAtomKind::Text(galley) => {
ui.painter().galley(rect.min, galley, fallback_text_color); ui.painter().galley(rect.min, galley, fallback_text_color);
} }
SizedAtomKind::Image(image, _) => { SizedAtomKind::Image { image, size: _ } => {
image.paint_at(ui, rect); image.paint_at(ui, rect);
} }
SizedAtomKind::Custom(id) => { SizedAtomKind::Empty { .. } => {}
debug_assert!(
!response.custom_rects.iter().any(|(i, _)| *i == id),
"Duplicate custom id"
);
response.custom_rects.push((id, rect));
}
SizedAtomKind::Empty => {}
} }
} }
@@ -450,7 +490,7 @@ impl<'atom> AllocatedAtomLayout<'atom> {
/// Response from a [`AtomLayout::show`] or [`AllocatedAtomLayout::paint`]. /// Response from a [`AtomLayout::show`] or [`AllocatedAtomLayout::paint`].
/// ///
/// Use [`AtomLayoutResponse::rect`] to get the response rects from [`AtomKind::Custom`]. /// Use [`AtomLayoutResponse::rect`] to get the response rects from [`crate::Atom::custom`].
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct AtomLayoutResponse { pub struct AtomLayoutResponse {
pub response: Response, pub response: Response,
@@ -470,7 +510,7 @@ impl AtomLayoutResponse {
self.custom_rects.iter().copied() self.custom_rects.iter().copied()
} }
/// Use this together with [`AtomKind::Custom`] to add custom painting / child widgets. /// Use this together with [`crate::Atom::custom`] to add custom painting / child widgets.
/// ///
/// NOTE: Don't `unwrap` rects, they might be empty when the widget is not visible. /// NOTE: Don't `unwrap` rects, they might be empty when the widget is not visible.
pub fn rect(&self, id: Id) -> Option<Rect> { pub fn rect(&self, id: Id) -> Option<Rect> {

View File

@@ -21,11 +21,26 @@ impl<'a> Atoms<'a> {
self.0.push(atom.into()); self.0.push(atom.into());
} }
/// Extend the list of atoms by appending more atoms to the right side.
///
/// If you have weird lifetime issues with this, use [`Self::push_right`] in a loop instead.
pub fn extend_right(&mut self, atoms: Self) {
self.0.extend(atoms.0);
}
/// Insert a new [`Atom`] at the beginning of the list (left side). /// Insert a new [`Atom`] at the beginning of the list (left side).
pub fn push_left(&mut self, atom: impl Into<Atom<'a>>) { pub fn push_left(&mut self, atom: impl Into<Atom<'a>>) {
self.0.insert(0, atom.into()); self.0.insert(0, atom.into());
} }
/// Extend the list of atoms by prepending more atoms to the left side.
///
/// If you have weird lifetime issues with this, use [`Self::push_left`] in a loop instead.
pub fn extend_left(&mut self, mut atoms: Self) {
std::mem::swap(&mut atoms.0, &mut self.0);
self.0.extend(atoms.0);
}
/// Concatenate and return the text contents. /// Concatenate and return the text contents.
// TODO(lucasmerlin): It might not always make sense to return the concatenated text, e.g. // TODO(lucasmerlin): It might not always make sense to return the concatenated text, e.g.
// in a submenu button there is a right text '⏵' which is now passed to the screen reader. // in a submenu button there is a right text '⏵' which is now passed to the screen reader.

View File

@@ -4,6 +4,8 @@ use emath::Vec2;
/// A [`crate::Atom`] which has been sized. /// A [`crate::Atom`] which has been sized.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct SizedAtom<'a> { pub struct SizedAtom<'a> {
pub id: Option<crate::Id>,
pub(crate) grow: bool, pub(crate) grow: bool,
/// The size of the atom. /// The size of the atom.
@@ -15,6 +17,9 @@ pub struct SizedAtom<'a> {
/// Intrinsic size of the atom. This is used to calculate `Response::intrinsic_size`. /// Intrinsic size of the atom. This is used to calculate `Response::intrinsic_size`.
pub intrinsic_size: Vec2, pub intrinsic_size: Vec2,
/// How will the atom be aligned in its available space?
pub align: emath::Align2,
pub kind: SizedAtomKind<'a>, pub kind: SizedAtomKind<'a>,
} }

View File

@@ -1,16 +1,20 @@
use crate::{Id, Image}; use crate::Image;
use emath::Vec2; use emath::Vec2;
use epaint::Galley; use epaint::Galley;
use std::sync::Arc; use std::sync::Arc;
/// A sized [`crate::AtomKind`]. /// A sized [`crate::AtomKind`].
#[derive(Clone, Default, Debug)] #[derive(Clone, Debug)]
pub enum SizedAtomKind<'a> { pub enum SizedAtomKind<'a> {
#[default] Empty { size: Option<Vec2> },
Empty,
Text(Arc<Galley>), Text(Arc<Galley>),
Image(Image<'a>, Vec2), Image { image: Image<'a>, size: Vec2 },
Custom(Id), }
impl Default for SizedAtomKind<'_> {
fn default() -> Self {
Self::Empty { size: None }
}
} }
impl SizedAtomKind<'_> { impl SizedAtomKind<'_> {
@@ -18,8 +22,8 @@ impl SizedAtomKind<'_> {
pub fn size(&self) -> Vec2 { pub fn size(&self) -> Vec2 {
match self { match self {
SizedAtomKind::Text(galley) => galley.size(), SizedAtomKind::Text(galley) => galley.size(),
SizedAtomKind::Image(_, size) => *size, SizedAtomKind::Image { image: _, size } => *size,
SizedAtomKind::Empty | SizedAtomKind::Custom(_) => Vec2::ZERO, SizedAtomKind::Empty { size } => size.unwrap_or_default(),
} }
} }
} }

View File

@@ -1198,10 +1198,9 @@ impl Prepared {
// Clear scroll delta so no parent scroll will use it: // Clear scroll delta so no parent scroll will use it:
ui.input_mut(|input| { ui.input_mut(|input| {
if always_scroll_enabled_direction { if always_scroll_enabled_direction {
input.smooth_scroll_delta()[0] = 0.0; input.smooth_scroll_delta = Vec2::ZERO;
input.smooth_scroll_delta()[1] = 0.0;
} else { } else {
input.smooth_scroll_delta()[d] = 0.0; input.smooth_scroll_delta[d] = 0.0;
} }
}); });

View File

@@ -2616,6 +2616,7 @@ impl ContextImpl {
platform_output.accesskit_update = Some(accesskit::TreeUpdate { platform_output.accesskit_update = Some(accesskit::TreeUpdate {
nodes, nodes,
tree: Some(accesskit::Tree::new(root_id)), tree: Some(accesskit::Tree::new(root_id)),
tree_id: accesskit::TreeId::ROOT,
focus: focus_id, focus: focus_id,
}); });
} }
@@ -3262,7 +3263,7 @@ impl Context {
for (name, data) in &mut font_definitions.font_data { for (name, data) in &mut font_definitions.font_data {
ui.collapsing(name, |ui| { ui.collapsing(name, |ui| {
let mut tweak = data.tweak; let mut tweak = data.tweak.clone();
if tweak.ui(ui).changed() { if tweak.ui(ui).changed() {
Arc::make_mut(data).tweak = tweak; Arc::make_mut(data).tweak = tweak;
changed = true; changed = true;

View File

@@ -253,9 +253,28 @@ pub struct ViewportInfo {
/// ///
/// This should be the same as [`RawInput::focused`]. /// This should be the same as [`RawInput::focused`].
pub focused: Option<bool>, pub focused: Option<bool>,
/// Is the window fully occluded (completely covered) by another window?
///
/// Not all platforms support this.
/// On platforms that don't, this will be `None` or `Some(false)`.
pub occluded: Option<bool>,
} }
impl ViewportInfo { impl ViewportInfo {
/// Is the window considered visible for rendering purposes?
///
/// A window is not visible if it is minimized or occluded.
/// When not visible, the UI is not painted and rendering is skipped,
/// but application logic may still be executed by some integrations.
pub fn visible(&self) -> Option<bool> {
match (self.minimized, self.occluded) {
(Some(true), _) | (_, Some(true)) => Some(false),
(Some(false), Some(false)) => Some(true),
(_, None) | (None, _) => None,
}
}
/// This viewport has been told to close. /// This viewport has been told to close.
/// ///
/// If this is the root viewport, the application will exit /// If this is the root viewport, the application will exit
@@ -282,6 +301,7 @@ impl ViewportInfo {
maximized: self.maximized, maximized: self.maximized,
fullscreen: self.fullscreen, fullscreen: self.fullscreen,
focused: self.focused, focused: self.focused,
occluded: self.occluded,
} }
} }
@@ -298,6 +318,7 @@ impl ViewportInfo {
maximized, maximized,
fullscreen, fullscreen,
focused, focused,
occluded,
} = self; } = self;
crate::Grid::new("viewport_info").show(ui, |ui| { crate::Grid::new("viewport_info").show(ui, |ui| {
@@ -345,6 +366,16 @@ impl ViewportInfo {
ui.label(opt_as_str(focused)); ui.label(opt_as_str(focused));
ui.end_row(); ui.end_row();
ui.label("Occluded:");
ui.label(opt_as_str(occluded));
ui.end_row();
let visible = self.visible();
ui.label("Visible:");
ui.label(opt_as_str(&visible));
ui.end_row();
fn opt_rect_as_string(v: &Option<Rect>) -> String { fn opt_rect_as_string(v: &Option<Rect>) -> String {
v.as_ref().map_or(String::new(), |r| { v.as_ref().map_or(String::new(), |r| {
format!("Pos: {:?}, size: {:?}", r.min, r.size()) format!("Pos: {:?}, size: {:?}", r.min, r.size())

View File

@@ -79,7 +79,7 @@ impl Id {
self.0.get() self.0.get()
} }
pub(crate) fn accesskit_id(&self) -> accesskit::NodeId { pub fn accesskit_id(&self) -> accesskit::NodeId {
self.value().into() self.value().into()
} }

View File

@@ -661,6 +661,8 @@ impl InputState {
if self.pointer.wants_repaint() if self.pointer.wants_repaint()
|| self.wheel.unprocessed_wheel_delta.abs().max_elem() > 0.2 || self.wheel.unprocessed_wheel_delta.abs().max_elem() > 0.2
|| !self.events.is_empty() || !self.events.is_empty()
|| !self.raw.hovered_files.is_empty()
|| !self.raw.dropped_files.is_empty()
{ {
// Immediate repaint // Immediate repaint
return Some(Duration::ZERO); return Some(Duration::ZERO);
@@ -869,7 +871,8 @@ impl InputState {
let accesskit_id = id.accesskit_id(); let accesskit_id = id.accesskit_id();
self.events.iter().filter_map(move |event| { self.events.iter().filter_map(move |event| {
if let Event::AccessKitActionRequest(request) = event if let Event::AccessKitActionRequest(request) = event
&& request.target == accesskit_id && request.target_node == accesskit_id
&& request.target_tree == accesskit::TreeId::ROOT
&& request.action == action && request.action == action
{ {
return Some(request); return Some(request);
@@ -886,7 +889,8 @@ impl InputState {
let accesskit_id = id.accesskit_id(); let accesskit_id = id.accesskit_id();
self.events.retain(|event| { self.events.retain(|event| {
if let Event::AccessKitActionRequest(request) = event if let Event::AccessKitActionRequest(request) = event
&& request.target == accesskit_id && request.target_node == accesskit_id
&& request.target_tree == accesskit::TreeId::ROOT
{ {
return !consume(request); return !consume(request);
} }

View File

@@ -685,7 +685,7 @@ pub fn __run_test_ctx(mut run_ui: impl FnMut(&Context)) {
} }
/// For use in tests; especially doctests. /// For use in tests; especially doctests.
pub fn __run_test_ui(add_contents: impl Fn(&mut Ui)) { pub fn __run_test_ui(mut add_contents: impl FnMut(&mut Ui)) {
let ctx = Context::default(); let ctx = Context::default();
ctx.set_fonts(FontDefinitions::empty()); // prevent fonts from being loaded (save CPU time) ctx.set_fonts(FontDefinitions::empty()); // prevent fonts from being loaded (save CPU time)
let _ = ctx.run_ui(Default::default(), |ui| { let _ = ctx.run_ui(Default::default(), |ui| {

View File

@@ -564,11 +564,13 @@ impl Focus {
if let crate::Event::AccessKitActionRequest(accesskit::ActionRequest { if let crate::Event::AccessKitActionRequest(accesskit::ActionRequest {
action: accesskit::Action::Focus, action: accesskit::Action::Focus,
target, target_node,
target_tree,
data: None, data: None,
}) = event }) = event
&& *target_tree == accesskit::TreeId::ROOT
{ {
self.id_requested_by_accesskit = Some(*target); self.id_requested_by_accesskit = Some(*target_node);
} }
} }
} }

View File

@@ -1,7 +1,11 @@
//! egui theme (spacing, colors, etc). //! egui theme (spacing, colors, etc).
use emath::Align; use emath::Align;
use epaint::{AlphaFromCoverage, CornerRadius, Shadow, Stroke, TextOptions, text::FontTweak}; use epaint::{
AlphaFromCoverage, CornerRadius, Shadow, Stroke, TextOptions,
mutex::Mutex,
text::{FontTweak, Tag},
};
use std::{collections::BTreeMap, ops::RangeInclusive, sync::Arc}; use std::{collections::BTreeMap, ops::RangeInclusive, sync::Arc};
use crate::{ use crate::{
@@ -2837,7 +2841,7 @@ impl Widget for &mut crate::Frame {
impl Widget for &mut FontTweak { impl Widget for &mut FontTweak {
fn ui(self, ui: &mut Ui) -> Response { fn ui(self, ui: &mut Ui) -> Response {
let original: FontTweak = *self; let original: FontTweak = self.clone();
let mut response = Grid::new("font_tweak") let mut response = Grid::new("font_tweak")
.num_columns(2) .num_columns(2)
@@ -2847,6 +2851,7 @@ impl Widget for &mut FontTweak {
y_offset_factor, y_offset_factor,
y_offset, y_offset,
hinting_override, hinting_override,
coords,
} = self; } = self;
ui.label("Scale"); ui.label("Scale");
@@ -2874,6 +2879,50 @@ impl Widget for &mut FontTweak {
ui.selectable_value(hinting_override, Some(true), "Enable"); ui.selectable_value(hinting_override, Some(true), "Enable");
ui.selectable_value(hinting_override, Some(false), "Disable"); ui.selectable_value(hinting_override, Some(false), "Disable");
}); });
ui.end_row();
ui.label("coords");
ui.end_row();
let mut to_remove = None;
for (i, (tag, value)) in coords.as_mut().iter_mut().enumerate() {
let tag_text = ui.ctx().data_mut(|data| {
let tag = *tag;
Arc::clone(data.get_temp_mut_or_insert_with(ui.id().with(i), move || {
Arc::new(Mutex::new(tag.to_string()))
}))
});
let tag_text = &mut *tag_text.lock();
let response = ui.text_edit_singleline(tag_text);
if response.changed()
&& let Ok(new_tag) = Tag::new_checked(tag_text.as_bytes())
{
*tag = new_tag;
}
// Reset stale text when not actively editing
// (e.g. after an item was removed and indices shifted)
if !response.has_focus()
&& Tag::new_checked(tag_text.as_bytes()).ok() != Some(*tag)
{
*tag_text = tag.to_string();
}
ui.add(DragValue::new(value));
if ui.small_button("🗑").clicked() {
to_remove = Some(i);
}
ui.end_row();
}
if let Some(i) = to_remove {
coords.remove(i);
}
if ui.button("Add coord").clicked() {
coords.push(b"wght", 0.0);
}
if ui.button("Clear coords").clicked() {
coords.clear();
}
ui.end_row();
if ui.button("Reset").clicked() { if ui.button("Reset").clicked() {
*self = Default::default(); *self = Default::default();

View File

@@ -4,6 +4,26 @@ use crate::{Context, Galley, Id};
use super::{CCursorRange, text_cursor_state::is_word_char}; use super::{CCursorRange, text_cursor_state::is_word_char};
/// AccessKit's `word_starts` uses `u8` indices, so text runs cannot exceed this length.
pub(crate) const MAX_CHARS_PER_TEXT_RUN: usize = 255;
/// Convert a (row, column) layout cursor position to a text run node ID and character index,
/// accounting for rows that are split into multiple text runs.
fn text_run_position(parent_id: Id, row: usize, column: usize) -> accesskit::TextPosition {
// When column lands exactly on a chunk boundary (e.g., 255), it refers to
// the end of the previous chunk, not the start of a new one.
let chunk_index = if column > 0 && column.is_multiple_of(MAX_CHARS_PER_TEXT_RUN) {
column / MAX_CHARS_PER_TEXT_RUN - 1
} else {
column / MAX_CHARS_PER_TEXT_RUN
};
let character_index = column - chunk_index * MAX_CHARS_PER_TEXT_RUN;
accesskit::TextPosition {
node: parent_id.with(row).with(chunk_index).accesskit_id(),
character_index,
}
}
/// Update accesskit with the current text state. /// Update accesskit with the current text state.
pub fn update_accesskit_for_text_widget( pub fn update_accesskit_for_text_widget(
ctx: &Context, ctx: &Context,
@@ -20,14 +40,8 @@ pub fn update_accesskit_for_text_widget(
let anchor = galley.layout_from_cursor(cursor_range.secondary); let anchor = galley.layout_from_cursor(cursor_range.secondary);
let focus = galley.layout_from_cursor(cursor_range.primary); let focus = galley.layout_from_cursor(cursor_range.primary);
builder.set_text_selection(accesskit::TextSelection { builder.set_text_selection(accesskit::TextSelection {
anchor: accesskit::TextPosition { anchor: text_run_position(parent_id, anchor.row, anchor.column),
node: parent_id.with(anchor.row).accesskit_id(), focus: text_run_position(parent_id, focus.row, focus.column),
character_index: anchor.column,
},
focus: accesskit::TextPosition {
node: parent_id.with(focus.row).accesskit_id(),
character_index: focus.column,
},
}); });
} }
@@ -40,61 +54,144 @@ pub fn update_accesskit_for_text_widget(
return; return;
}; };
let mut prev_row_ended_with_newline = true;
for (row_index, row) in galley.rows.iter().enumerate() { for (row_index, row) in galley.rows.iter().enumerate() {
let row_id = parent_id.with(row_index); let glyph_count = row.glyphs.len();
let mut value = String::with_capacity(glyph_count);
let mut character_lengths = Vec::<u8>::with_capacity(glyph_count);
let mut character_positions = Vec::<f32>::with_capacity(glyph_count);
let mut character_widths = Vec::<f32>::with_capacity(glyph_count);
let mut word_starts = Vec::<usize>::new();
// For soft-wrapped continuation rows, treat the start as a word
// boundary so the first word character gets a `word_starts` entry.
// Paragraph-starting runs (first row or after a newline) get an
// implicit word start from AccessKit, so they don't need this.
let mut was_at_word_end = !prev_row_ended_with_newline;
ctx.register_accesskit_parent(row_id, parent_id); for glyph in &row.glyphs {
let is_word_char = is_word_char(glyph.chr);
if is_word_char && was_at_word_end {
word_starts.push(character_lengths.len());
}
was_at_word_end = !is_word_char;
let old_len = value.len();
value.push(glyph.chr);
character_lengths.push((value.len() - old_len) as _);
character_positions.push(glyph.pos.x - row.pos.x);
character_widths.push(glyph.advance_width);
}
ctx.accesskit_node_builder(row_id, |builder| { if row.ends_with_newline {
builder.set_role(accesskit::Role::TextRun); value.push('\n');
let rect = global_from_galley * row.rect_without_leading_space(); character_lengths.push(1);
builder.set_bounds(accesskit::Rect { character_positions.push(row.size.x);
x0: rect.min.x.into(), character_widths.push(0.0);
y0: rect.min.y.into(), }
x1: rect.max.x.into(),
y1: rect.max.y.into(),
});
builder.set_text_direction(accesskit::TextDirection::LeftToRight);
// TODO(mwcampbell): Set more node fields for the row
// once AccessKit adapters expose text formatting info.
let glyph_count = row.glyphs.len(); let total_chars = character_lengths.len();
let mut value = String::new();
value.reserve(glyph_count);
let mut character_lengths = Vec::<u8>::with_capacity(glyph_count);
let mut character_positions = Vec::<f32>::with_capacity(glyph_count);
let mut character_widths = Vec::<f32>::with_capacity(glyph_count);
let mut word_lengths = Vec::<u8>::new();
let mut was_at_word_end = false;
let mut last_word_start = 0usize;
for glyph in &row.glyphs { if total_chars <= MAX_CHARS_PER_TEXT_RUN {
let is_word_char = is_word_char(glyph.chr); let run_id = parent_id.with(row_index).with(0usize);
if is_word_char && was_at_word_end { ctx.register_accesskit_parent(run_id, parent_id);
word_lengths.push((character_lengths.len() - last_word_start) as _);
last_word_start = character_lengths.len(); ctx.accesskit_node_builder(run_id, |builder| {
builder.set_role(accesskit::Role::TextRun);
builder.set_text_direction(accesskit::TextDirection::LeftToRight);
// TODO(mwcampbell): Set more node fields for the row
// once AccessKit adapters expose text formatting info.
let rect = global_from_galley * row.rect_without_leading_space();
builder.set_bounds(accesskit::Rect {
x0: rect.min.x.into(),
y0: rect.min.y.into(),
x1: rect.max.x.into(),
y1: rect.max.y.into(),
});
builder.set_value(value);
builder.set_character_lengths(character_lengths);
let pos_offset = character_positions.first().copied().unwrap_or(0.0);
for p in &mut character_positions {
*p -= pos_offset;
} }
was_at_word_end = !is_word_char; builder.set_character_positions(character_positions);
let old_len = value.len(); builder.set_character_widths(character_widths);
value.push(glyph.chr);
character_lengths.push((value.len() - old_len) as _);
character_positions.push(glyph.pos.x - row.pos.x);
character_widths.push(glyph.advance_width);
}
if row.ends_with_newline { let chunk_word_starts: Vec<u8> = word_starts.iter().map(|&ws| ws as u8).collect();
value.push('\n'); builder.set_word_starts(chunk_word_starts);
character_lengths.push(1); });
character_positions.push(row.size.x); } else {
character_widths.push(0.0); let num_chunks = total_chars.div_ceil(MAX_CHARS_PER_TEXT_RUN);
} let mut byte_offset = 0usize;
word_lengths.push((character_lengths.len() - last_word_start) as _);
builder.set_value(value); for chunk_idx in 0..num_chunks {
builder.set_character_lengths(character_lengths); let char_start = chunk_idx * MAX_CHARS_PER_TEXT_RUN;
builder.set_character_positions(character_positions); let char_end = (char_start + MAX_CHARS_PER_TEXT_RUN).min(total_chars);
builder.set_character_widths(character_widths);
builder.set_word_lengths(word_lengths); let byte_start = byte_offset;
}); let chunk_byte_len: usize = character_lengths[char_start..char_end]
.iter()
.map(|&l| l as usize)
.sum();
let byte_end = byte_start + chunk_byte_len;
byte_offset = byte_end;
let run_id = parent_id.with(row_index).with(chunk_idx);
ctx.register_accesskit_parent(run_id, parent_id);
ctx.accesskit_node_builder(run_id, |builder| {
builder.set_role(accesskit::Role::TextRun);
builder.set_text_direction(accesskit::TextDirection::LeftToRight);
// TODO(mwcampbell): Set more node fields for the row
// once AccessKit adapters expose text formatting info.
if chunk_idx > 0 {
let prev_id = parent_id.with(row_index).with(chunk_idx - 1);
builder.set_previous_on_line(prev_id.accesskit_id());
}
if chunk_idx + 1 < num_chunks {
let next_id = parent_id.with(row_index).with(chunk_idx + 1);
builder.set_next_on_line(next_id.accesskit_id());
}
let row_rect = row.rect_without_leading_space();
let chunk_x0 = row.pos.x + character_positions[char_start];
let chunk_x1 = row.pos.x
+ character_positions[char_end - 1]
+ character_widths[char_end - 1];
let chunk_rect = emath::Rect::from_min_max(
emath::pos2(chunk_x0, row_rect.min.y),
emath::pos2(chunk_x1, row_rect.max.y),
);
let rect = global_from_galley * chunk_rect;
builder.set_bounds(accesskit::Rect {
x0: rect.min.x.into(),
y0: rect.min.y.into(),
x1: rect.max.x.into(),
y1: rect.max.y.into(),
});
builder.set_value(value[byte_start..byte_end].to_owned());
builder.set_character_lengths(character_lengths[char_start..char_end].to_vec());
let pos_offset = character_positions[char_start];
let chunk_positions: Vec<f32> = character_positions[char_start..char_end]
.iter()
.map(|&p| p - pos_offset)
.collect();
builder.set_character_positions(chunk_positions);
builder.set_character_widths(character_widths[char_start..char_end].to_vec());
let chunk_word_starts: Vec<u8> = word_starts
.iter()
.filter(|&&ws| ws >= char_start && ws < char_end)
.map(|&ws| (ws - char_start) as u8)
.collect();
builder.set_word_starts(chunk_word_starts);
});
}
}
prev_row_ended_with_newline = row.ends_with_newline;
} }
} }

View File

@@ -192,10 +192,13 @@ impl CCursorRange {
Event::AccessKitActionRequest(accesskit::ActionRequest { Event::AccessKitActionRequest(accesskit::ActionRequest {
action: accesskit::Action::SetTextSelection, action: accesskit::Action::SetTextSelection,
target, target_node,
target_tree,
data: Some(accesskit::ActionData::SetTextSelection(selection)), data: Some(accesskit::ActionData::SetTextSelection(selection)),
}) => { }) => {
if _widget_id.accesskit_id() == *target { if _widget_id.accesskit_id() == *target_node
&& *target_tree == accesskit::TreeId::ROOT
{
let primary = let primary =
ccursor_from_accesskit_text_position(_widget_id, galley, &selection.focus); ccursor_from_accesskit_text_position(_widget_id, galley, &selection.focus);
let secondary = let secondary =
@@ -224,18 +227,31 @@ fn ccursor_from_accesskit_text_position(
galley: &Galley, galley: &Galley,
position: &accesskit::TextPosition, position: &accesskit::TextPosition,
) -> Option<CCursor> { ) -> Option<CCursor> {
use super::accesskit_text::MAX_CHARS_PER_TEXT_RUN;
let mut total_length = 0usize; let mut total_length = 0usize;
for (i, row) in galley.rows.iter().enumerate() { for (i, row) in galley.rows.iter().enumerate() {
let row_id = id.with(i); let row_chars = row.glyphs.len() + (row.ends_with_newline as usize);
if row_id.accesskit_id() == position.node { let num_chunks = if row_chars == 0 {
return Some(CCursor { 1
index: total_length + position.character_index, } else {
prefer_next_row: !(position.character_index == row.glyphs.len() row_chars.div_ceil(MAX_CHARS_PER_TEXT_RUN)
&& !row.ends_with_newline };
&& (i + 1) < galley.rows.len()),
}); for chunk_idx in 0..num_chunks {
let run_id = id.with(i).with(chunk_idx);
if run_id.accesskit_id() == position.node {
let column = chunk_idx * MAX_CHARS_PER_TEXT_RUN + position.character_index;
return Some(CCursor {
index: total_length + column,
prefer_next_row: !(column == row.glyphs.len()
&& !row.ends_with_newline
&& (i + 1) < galley.rows.len()),
});
}
} }
total_length += row.glyphs.len() + (row.ends_with_newline as usize);
total_length += row_chars;
} }
None None
} }

View File

@@ -67,9 +67,7 @@ pub fn paint_text_selection(
let first_vertex_index = row let first_vertex_index = row
.glyphs .glyphs
.get(first_glyph_index) .get(first_glyph_index)
.map_or(row.visuals.glyph_vertex_range.start, |g| { .map_or(row.visuals.glyph_vertex_range.end, |g| g.first_vertex as _);
g.first_vertex as _
});
let last_vertex_index = row let last_vertex_index = row
.glyphs .glyphs
.get(last_glyph_index) .get(last_glyph_index)

View File

@@ -54,8 +54,8 @@ impl UiBuilder {
/// ///
/// This is a shortcut for `.id_salt(my_id).global_scope(true)`. /// This is a shortcut for `.id_salt(my_id).global_scope(true)`.
#[inline] #[inline]
pub fn id(mut self, id: impl Hash) -> Self { pub fn id(mut self, id: Id) -> Self {
self.id_salt = Some(Id::new(id)); self.id_salt = Some(id);
self.global_scope = true; self.global_scope = true;
self self
} }

View File

@@ -1,5 +1,5 @@
use emath::GuiRounding as _; use emath::GuiRounding as _;
use epaint::text::TextFormat; use epaint::text::{IntoTag, TextFormat, VariationCoords};
use std::fmt::Formatter; use std::fmt::Formatter;
use std::{borrow::Cow, sync::Arc}; use std::{borrow::Cow, sync::Arc};
@@ -34,6 +34,7 @@ pub struct RichText {
background_color: Color32, background_color: Color32,
expand_bg: f32, expand_bg: f32,
text_color: Option<Color32>, text_color: Option<Color32>,
coords: VariationCoords,
code: bool, code: bool,
strong: bool, strong: bool,
weak: bool, weak: bool,
@@ -55,6 +56,7 @@ impl Default for RichText {
background_color: Default::default(), background_color: Default::default(),
expand_bg: 1.0, expand_bg: 1.0,
text_color: Default::default(), text_color: Default::default(),
coords: Default::default(),
code: Default::default(), code: Default::default(),
strong: Default::default(), strong: Default::default(),
weak: Default::default(), weak: Default::default(),
@@ -196,6 +198,23 @@ impl RichText {
self self
} }
/// Add a variation coordinate.
#[inline]
pub fn variation(mut self, tag: impl IntoTag, coord: f32) -> Self {
self.coords.push(tag, coord);
self
}
/// Override the variation coordinates completely.
#[inline]
pub fn variations<T: IntoTag>(
mut self,
variations: impl IntoIterator<Item = (T, f32)>,
) -> Self {
self.coords = VariationCoords::new(variations);
self
}
/// Override the [`TextStyle`]. /// Override the [`TextStyle`].
#[inline] #[inline]
pub fn text_style(mut self, text_style: TextStyle) -> Self { pub fn text_style(mut self, text_style: TextStyle) -> Self {
@@ -391,6 +410,7 @@ impl RichText {
background_color, background_color,
expand_bg, expand_bg,
text_color: _, // already used by `get_text_color` text_color: _, // already used by `get_text_color`
coords,
code, code,
strong: _, // already used by `get_text_color` strong: _, // already used by `get_text_color`
weak: _, // already used by `get_text_color` weak: _, // already used by `get_text_color`
@@ -449,6 +469,7 @@ impl RichText {
line_height, line_height,
color: text_color, color: text_color,
background: background_color, background: background_color,
coords,
italics, italics,
underline, underline,
strikethrough, strikethrough,

View File

@@ -259,6 +259,13 @@ impl<'a> Button<'a> {
self self
} }
/// Set the gap between atoms.
#[inline]
pub fn gap(mut self, gap: f32) -> Self {
self.layout = self.layout.gap(gap);
self
}
/// Show the button and return a [`AtomLayoutResponse`] for painting custom contents. /// Show the button and return a [`AtomLayoutResponse`] for painting custom contents.
pub fn atom_ui(self, ui: &mut Ui) -> AtomLayoutResponse { pub fn atom_ui(self, ui: &mut Ui) -> AtomLayoutResponse {
let Button { let Button {

View File

@@ -1,11 +1,10 @@
#![expect(clippy::needless_pass_by_value)] // False positives with `impl ToString`
use std::{cmp::Ordering, ops::RangeInclusive};
use crate::{ use crate::{
Button, CursorIcon, Id, Key, MINUS_CHAR_STR, Modifiers, NumExt as _, Response, RichText, Sense, Atom, AtomExt as _, AtomKind, Atoms, Button, CursorIcon, Id, IntoAtoms, Key, MINUS_CHAR_STR,
TextEdit, TextWrapMode, Ui, Widget, WidgetInfo, emath, text, Modifiers, NumExt as _, Response, RichText, Sense, TextEdit, TextWrapMode, Ui, Widget,
WidgetInfo, emath, text,
}; };
use emath::Vec2;
use std::{cmp::Ordering, ops::RangeInclusive};
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@@ -38,8 +37,7 @@ fn set(get_set_value: &mut GetSetValue<'_>, value: f64) {
pub struct DragValue<'a> { pub struct DragValue<'a> {
get_set_value: GetSetValue<'a>, get_set_value: GetSetValue<'a>,
speed: f64, speed: f64,
prefix: String, atoms: Atoms<'a>,
suffix: String,
range: RangeInclusive<f64>, range: RangeInclusive<f64>,
clamp_existing_to_range: bool, clamp_existing_to_range: bool,
min_decimals: usize, min_decimals: usize,
@@ -50,6 +48,8 @@ pub struct DragValue<'a> {
} }
impl<'a> DragValue<'a> { impl<'a> DragValue<'a> {
const ATOM_ID: &'static str = "drag_item";
pub fn new<Num: emath::Numeric>(value: &'a mut Num) -> Self { pub fn new<Num: emath::Numeric>(value: &'a mut Num) -> Self {
let slf = Self::from_get_set(move |v: Option<f64>| { let slf = Self::from_get_set(move |v: Option<f64>| {
if let Some(v) = v { if let Some(v) = v {
@@ -66,11 +66,12 @@ impl<'a> DragValue<'a> {
} }
pub fn from_get_set(get_set_value: impl 'a + FnMut(Option<f64>) -> f64) -> Self { pub fn from_get_set(get_set_value: impl 'a + FnMut(Option<f64>) -> f64) -> Self {
let atoms = Atoms::new(Atom::custom(Id::new(Self::ATOM_ID), Vec2::ZERO).atom_grow(true));
Self { Self {
get_set_value: Box::new(get_set_value), get_set_value: Box::new(get_set_value),
speed: 1.0, speed: 1.0,
prefix: Default::default(), atoms,
suffix: Default::default(),
range: f64::NEG_INFINITY..=f64::INFINITY, range: f64::NEG_INFINITY..=f64::INFINITY,
clamp_existing_to_range: true, clamp_existing_to_range: true,
min_decimals: 0, min_decimals: 0,
@@ -164,15 +165,15 @@ impl<'a> DragValue<'a> {
/// Show a prefix before the number, e.g. "x: " /// Show a prefix before the number, e.g. "x: "
#[inline] #[inline]
pub fn prefix(mut self, prefix: impl ToString) -> Self { pub fn prefix(mut self, prefix: impl IntoAtoms<'a>) -> Self {
self.prefix = prefix.to_string(); self.atoms.extend_left(prefix.into_atoms());
self self
} }
/// Add a suffix to the number, this can be e.g. a unit ("°" or " m") /// Add a suffix to the number, this can be e.g. a unit ("°" or " m")
#[inline] #[inline]
pub fn suffix(mut self, suffix: impl ToString) -> Self { pub fn suffix(mut self, suffix: impl IntoAtoms<'a>) -> Self {
self.suffix = suffix.to_string(); self.atoms.extend_right(suffix.into_atoms());
self self
} }
@@ -433,8 +434,7 @@ impl Widget for DragValue<'_> {
speed, speed,
range, range,
clamp_existing_to_range, clamp_existing_to_range,
prefix, mut atoms,
suffix,
min_decimals, min_decimals,
max_decimals, max_decimals,
custom_formatter, custom_formatter,
@@ -442,6 +442,23 @@ impl Widget for DragValue<'_> {
update_while_editing, update_while_editing,
} = self; } = self;
let mut prefix_text = String::new();
let mut suffix_text = String::new();
let mut past_value = false;
let atom_id = Id::new(Self::ATOM_ID);
for atom in atoms.iter() {
if atom.id == Some(atom_id) {
past_value = true;
}
if let AtomKind::Text(text) = &atom.kind {
if past_value {
suffix_text.push_str(text.text());
} else {
prefix_text.push_str(text.text());
}
}
}
let shift = ui.input(|i| i.modifiers.shift_only()); let shift = ui.input(|i| i.modifiers.shift_only());
// The widget has the same ID whether it's in edit or button mode. // The widget has the same ID whether it's in edit or button mode.
let id = ui.next_auto_id(); let id = ui.next_auto_id();
@@ -543,8 +560,6 @@ impl Widget for DragValue<'_> {
} }
} }
// some clones below are redundant if AccessKit is disabled
#[expect(clippy::redundant_clone)]
let mut response = if is_kb_editing { let mut response = if is_kb_editing {
let mut value_text = ui let mut value_text = ui
.data_mut(|data| data.remove_temp::<String>(id)) .data_mut(|data| data.remove_temp::<String>(id))
@@ -586,13 +601,20 @@ impl Widget for DragValue<'_> {
ui.data_mut(|data| data.insert_temp(id, value_text)); ui.data_mut(|data| data.insert_temp(id, value_text));
response response
} else { } else {
let button = Button::new( atoms.map_atoms(|atom| {
RichText::new(format!("{}{}{}", prefix, value_text.clone(), suffix)) if atom.id == Some(atom_id) {
.text_style(text_style), RichText::new(value_text.clone())
) .text_style(text_style.clone())
.wrap_mode(TextWrapMode::Extend) .into()
.sense(Sense::click_and_drag()) } else {
.min_size(ui.spacing().interact_size); // TODO(emilk): find some more generic solution to `min_size` atom
}
});
let button = Button::new(atoms)
.wrap_mode(TextWrapMode::Extend)
.sense(Sense::click_and_drag())
.gap(0.0)
.min_size(ui.spacing().interact_size); // TODO(emilk): find some more generic solution to `min_size`
let cursor_icon = if value <= *range.start() { let cursor_icon = if value <= *range.start() {
CursorIcon::ResizeEast CursorIcon::ResizeEast
@@ -607,10 +629,8 @@ impl Widget for DragValue<'_> {
if ui.style().explanation_tooltips { if ui.style().explanation_tooltips {
response = response.on_hover_text(format!( response = response.on_hover_text(format!(
"{}{}{}\nDrag to edit or click to enter a value.\nPress 'Shift' while dragging for better control.", "{}\nDrag to edit or click to enter a value.\nPress 'Shift' while dragging for better control.",
prefix,
value as f32, // Show full precision value on-hover. TODO(emilk): figure out f64 vs f32 value as f32, // Show full precision value on-hover. TODO(emilk): figure out f64 vs f32
suffix
)); ));
} }
@@ -704,7 +724,7 @@ impl Widget for DragValue<'_> {
// The value is exposed as a string by the text edit widget // The value is exposed as a string by the text edit widget
// when in edit mode. // when in edit mode.
if !is_kb_editing { if !is_kb_editing {
let value_text = format!("{prefix}{value_text}{suffix}"); let value_text = format!("{prefix_text}{value_text}{suffix_text}");
builder.set_value(value_text); builder.set_value(value_text);
} }
}); });

View File

@@ -1,7 +1,7 @@
use std::mem; use std::mem;
use accesskit::{Action, ActionRequest, NodeId}; use accesskit::{Action, ActionRequest};
use accesskit_consumer::{FilterResult, Node, Tree, TreeChangeHandler}; use accesskit_consumer::{FilterResult, Node, NodeId, Tree, TreeChangeHandler};
use eframe::epaint::text::TextWrapMode; use eframe::epaint::text::TextWrapMode;
use egui::{ use egui::{
@@ -25,7 +25,7 @@ use egui::{
pub struct AccessibilityInspectorPlugin { pub struct AccessibilityInspectorPlugin {
pub open: bool, pub open: bool,
tree: Option<accesskit_consumer::Tree>, tree: Option<accesskit_consumer::Tree>,
selected_node: Option<Id>, selected_node: Option<NodeId>,
queued_action: Option<ActionRequest>, queued_action: Option<ActionRequest>,
} }
@@ -113,13 +113,17 @@ impl AccessibilityInspectorPlugin {
Id::new("Accessibility Inspector") Id::new("Accessibility Inspector")
} }
fn selection_ui(&mut self, ui: &mut Ui, selected_node: Id) { fn selection_ui(&mut self, ui: &mut Ui, selected_node: NodeId) {
ui.separator(); ui.separator();
if let Some(tree) = &self.tree if let Some(tree) = &self.tree
&& let Some(node) = tree.state().node_by_id(NodeId::from(selected_node.value())) && let Some(node) = tree.state().node_by_id(selected_node)
{ {
let node_response = ui.ctx().read_response(selected_node); // Safety: This is safe since the `accesskit::NodeId` was created from an `egui::Id`.
#[expect(unsafe_code)]
let egui_node_id = unsafe { Id::from_high_entropy_bits(node.locate().0.0) };
let node_response = ui.ctx().read_response(egui_node_id);
if let Some(widget_response) = node_response { if let Some(widget_response) = node_response {
ui.debug_painter().debug_rect( ui.debug_painter().debug_rect(
@@ -174,8 +178,10 @@ impl AccessibilityInspectorPlugin {
if node.supports_action(action, &|_node| FilterResult::Include) if node.supports_action(action, &|_node| FilterResult::Include)
&& ui.button(format!("{action:?}")).clicked() && ui.button(format!("{action:?}")).clicked()
{ {
let (target_node, target_tree) = node.locate();
let action_request = ActionRequest { let action_request = ActionRequest {
target: node.id(), target_node,
target_tree,
action, action,
data: None, data: None,
}; };
@@ -188,8 +194,8 @@ impl AccessibilityInspectorPlugin {
} }
} }
fn node_ui(ui: &mut Ui, node: &Node<'_>, selected_node: &mut Option<Id>) { fn node_ui(ui: &mut Ui, node: &Node<'_>, selected_node: &mut Option<NodeId>) {
if node.id() == Self::id().value().into() if node.locate() == (Self::id().value().into(), accesskit::TreeId::ROOT)
|| node || node
.value() .value()
.as_deref() .as_deref()
@@ -200,12 +206,12 @@ impl AccessibilityInspectorPlugin {
let label = node let label = node
.label() .label()
.or_else(|| node.value()) .or_else(|| node.value())
.unwrap_or_else(|| node.id().0.to_string()); .unwrap_or_else(|| node.locate().0.0.to_string());
let label = format!("({:?}) {}", node.role(), label); let label = format!("({:?}) {}", node.role(), label);
// Safety: This is safe since the `accesskit::NodeId` was created from an `egui::Id`. // Safety: This is safe since the `accesskit::NodeId` was created from an `egui::Id`.
#[expect(unsafe_code)] #[expect(unsafe_code)]
let egui_node_id = unsafe { Id::from_high_entropy_bits(node.id().0) }; let egui_node_id = unsafe { Id::from_high_entropy_bits(node.locate().0.0) };
ui.push_id(node.id(), |ui| { ui.push_id(node.id(), |ui| {
let child_count = node.children().len(); let child_count = node.children().len();
@@ -228,7 +234,7 @@ impl AccessibilityInspectorPlugin {
collapsing.set_open(!collapsing.is_open()); collapsing.set_open(!collapsing.is_open());
} }
let label_response = let label_response =
ui.selectable_value(selected_node, Some(egui_node_id), label.clone()); ui.selectable_value(selected_node, Some(node.id()), label.clone());
if label_response.hovered() { if label_response.hovered() {
let widget_response = ui.ctx().read_response(egui_node_id); let widget_response = ui.ctx().read_response(egui_node_id);

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:5878bc5beaf4406c24f23d900aa9ac7c5507e44cb3ade83b743b8b62e7da1615 oid sha256:e4cff85a005ad897624f6c7a2b2ad599325ec99e0b3c9c35963f16611f283997
size 335355 size 335371

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:706ad012e52a8c51175b050b985cca88e2cb306b24f618b7391641397d17cd28 oid sha256:cc55083688d043234c37d22c74635a44b00f4d28c3802c4327c2eaf563c73eed
size 92804 size 92800

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:4135662f2b60a10ef8c3b155172d7a3edcf24a625d8286aeaad0614aa8819893 oid sha256:78d91fa4657cd1cb375487f606b80d418ed6fdbd8a0c0225b9383eead5001563
size 169604 size 169682

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:509020d8885b718900e534c9948cb95ae88e1eee9e113bdfb77a2f75b9a68f7b oid sha256:c8b23a5286e5d2dbd8d3eddac6583d981152bd791f74edfa5c712a610f795256
size 96703 size 96759

View File

@@ -66,7 +66,8 @@ impl crate::View for TextEditDemo {
egui::Label::new("Press ctrl+Y to toggle the case of selected text (cmd+Y on Mac)"), egui::Label::new("Press ctrl+Y to toggle the case of selected text (cmd+Y on Mac)"),
); );
if ui.input_mut(|i| i.consume_key(egui::Modifiers::COMMAND, egui::Key::Y)) if output.response.has_focus()
&& ui.input_mut(|i| i.consume_key(egui::Modifiers::COMMAND, egui::Key::Y))
&& let Some(text_cursor_range) = output.cursor_range && let Some(text_cursor_range) = output.cursor_range
{ {
use egui::TextBuffer as _; use egui::TextBuffer as _;

View File

@@ -59,21 +59,27 @@ fn test_italics() {
#[test] #[test]
fn test_text_selection() { fn test_text_selection() {
let mut harness = Harness::builder().build_ui(|ui| { let mut results = egui_kittest::SnapshotResults::new();
let visuals = ui.visuals_mut();
visuals.selection.bg_fill = Color32::LIGHT_GREEN;
visuals.selection.stroke.color = Color32::DARK_BLUE;
ui.label("Some varied ☺ text :)\nAnd it has a second line!"); for (test_idx, drag_start_x) in [0.2_f32, 0.9].into_iter().enumerate() {
}); let mut harness = Harness::builder().build_ui(|ui| {
harness.run(); let visuals = ui.visuals_mut();
harness.fit_contents(); visuals.selection.bg_fill = Color32::LIGHT_GREEN;
visuals.selection.stroke.color = Color32::RED;
// Drag to select text: ui.label("Some varied ☺ text :)\nAnd it has a second line!");
let label = harness.get_by_role(Role::Label); });
harness.drag_at(label.rect().lerp_inside([0.2, 0.25])); harness.run();
harness.drop_at(label.rect().lerp_inside([0.6, 0.75])); harness.fit_contents();
harness.run();
harness.snapshot("text_selection"); // Drag to select text:
let label = harness.get_by_role(Role::Label);
harness.drag_at(label.rect().lerp_inside([drag_start_x, 0.25]));
harness.drop_at(label.rect().lerp_inside([0.6, 0.75]));
harness.run();
harness.snapshot(format!("text_selection_{test_idx}"));
results.extend_harness(&mut harness);
}
} }

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:52d2233594c4bad348f5479dcfad9576ee5fd7d49faedb6f5ba74b374cdaf3ad oid sha256:a53262cf5d8507d8eeae8c968767cef462b727879245085673982b850a6da670
size 26977 size 26977

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:47f6cd15b88df83a9b2d8538e424041e661712f2e85312166a581f69f1254643 oid sha256:75a9cd9a3315b236c23a53e890de1a821d39c3327813d06df85ba86d2ed50cc7
size 26839 size 26887

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:9302478abb0b86fae1af3af45d91f032272a56a2098405525d08aba4f9534644 oid sha256:a7601584308bf60820506f842569a3c1daf3c15fa6e715f6b9386b5112dcc92f
size 76103 size 76076

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:6927950ccbc5c81d6fbfe0a90ddd79a4306518caced14bb60debd30c7e41d326 oid sha256:d4e33c7f817100d8414bba245ee7886354b86109f383d59e87a197e39501f0a0
size 62604 size 62604

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:366d18457aabf1ebdd42fdbce8819cc67a4f59db85c452623b02ee1d0e8fc50a oid sha256:93fcc271831167cb077f3de0a9f0e27037f9e5a2ce94e056bd6f1ede9890cb7e
size 27817 size 27818

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:3c5e803659e936268b476690427ef6a6802f477e078dc956a9d1c857b48da868 oid sha256:3fc2793506ec483c7f124b6206fb18ffb73bec29746f2d9bb5145042ddc45016
size 114409 size 114410

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:f5e67baf0696792e50f7ab3121874d055ddee2de0514712aacbf8e135ec4743d oid sha256:20ea4f93ee50c7a3585aef74c66d7700083ac1c16519b0704b70387849d9d2bc
size 25425 size 25057

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:55c5fb90736a31fbccd72be5994fc8c62b4b9da9842ad1e6bb795a1e1461a6f8 oid sha256:1b72a4c0e6d441190a7a156b8bba709e81b6c1fe7b0eacedc1ee7a3bfcf881f6
size 98780 size 99297

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:a4080ee1a16eea16c8f4246fe3e760ade7d0289b30d88068d1e49ffb88d88dca oid sha256:08c40934d4bd2a239bdcc1928d1e5eba56bac03fdded2c85cf47b020d669f07e
size 18280 size 18281

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:600d9e0fc193396f36b599e4bfad2547128160d2e56dc2a989cb5f978d5115ae oid sha256:82878e4150e38fdc4b2e78203c8c661c2d9e716ab32595c298392faf6ba96105
size 113797 size 113803

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:3ae5e843cc9d847b0f3c4092f55b914699adb506cb807b0a97bfc4ec7d94537b oid sha256:58cd3aba4392332a45f57c7dd90a9b5da386cb396c0c6319e7a7dae71e03ff30
size 22613 size 22563

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:b0d38cb1eebf3ce7d661d094175b425db2b9eccc5e439b14256c5d801d4454d4 oid sha256:26ffcf6b71108b82ce15d4cf3f9dd0ce9fe0b9563f02725fef1b74f40e749439
size 47285 size 47281

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:00e4c7659cd50044d473dd2c138392f78ac7eba27f2b52bae61246f5dc5b2782 oid sha256:faedf9631149e231d510165215c24fccec50502d58000d5f893aa047a637a68f
size 23156 size 23148

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:56b44d26946770c0878e11e3197633697ad339a7e8fcffe7279a6b4c45cd3582 oid sha256:b6b4c2e55c02fa4caf5f9f8bd2d8c0311cc4cbcf1fc2f568fe112e8e6125c675
size 65384 size 65308

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:34de6fd788288174e8e6f1fa48cd49dbc7b14fcf649fe302aed49c8c50178aa8 oid sha256:3a65927cd8bd8d24e3ffbea8eb421eb22849b27dc77d36f8acd82bf5d5e63959
size 33469 size 33469

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:96760220222bdde8dd1b3d28f089af2892403b78df8d34d3d94dc1a604387083 oid sha256:9c595ee9b7ada33780178a6a35e26a98055a707f2ff99f6bb36e8db4ed819791
size 18241 size 18242

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:48d138634343edb251435bf6f9075502b913e806e8b280f3e6012977c13af16f oid sha256:c218115d305dfa6c9ab883ac6f3a21584b4840b3ba273ea765c8a8381d78935f
size 56753 size 57181

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:23efb79ca13367f4d8886142d015815c5bdf99c0ed243ece294a7cfd365fd166 oid sha256:4d10b78f4d80d61a3352d7f2b0ed9b2d93af5f184f2487f6f2afff02a38f4608
size 33503 size 33475

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:5d85faf6e7fa26741eb720e74695f3c207ea15097b118c3cafe5d52d5d85ea20 oid sha256:f2ce9062c5d1f0b0861d5df49ae64e56ba0e6501e8bd3f8a92c53aea748be78b
size 23666 size 23629

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:875eb687f3a1eed52a6617e532edc5332b0a16296e2b6addac66d5bea0448b14 oid sha256:b5b965a7c690fd8e8646812513e2417170b687fd37e29d220c29127ba0cc200c
size 172605 size 172609

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:fd3573be9ba5818b4edc371095f5c23b084e6c7eaae4f2fd3a6d2de051878c9d oid sha256:6ffba8bb50b42e47f855f62682f6d5ec10bf67b01d3aa2e843f6bf787f150d0d
size 118567 size 118562

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:ed1be0294fb65b11c54c6dc9e4cecb383ace16dad748e3c42f2ed65b2fb05ea8 oid sha256:931f38ade8373ff79801c05c5d4397f2c5fcfa27022f2e1abe9eb29d561a3aef
size 75509 size 76022

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:7336c53885add09360df098b6b131323e8ad3ef0ec2b85bf022e78bc4269276a oid sha256:57bf5220ae8f47485a07e9117abaaad36924d8c6c0f9e278cb05c455f342bff6
size 70255 size 70250

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:411dd61eb182a70d46c7fc1fa0f9a4b8aeae88d08b11d5af948c5acccfa9d133 oid sha256:7c964d07a39ad286a562b53cdfe514d568d91955e6c1ca06a0cb5e45dbe3977e
size 60950 size 60947

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:78111e33d44a09beb9c1233dd2d5ef10103213a1c1c7df8b5e258d9684f1d93a oid sha256:718203d31d8b027a7718a66c4712cf1e17b9aea2e870d755bd2c0c346529d4f4
size 21810 size 21814

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:e9498a706de403ee7db3603ecc896688e584fede367ed6087cdf10b798a3ab2d oid sha256:6af5adc42544171c6d85e190c853aca06784c131a373a693a6f7069d4cf1a404
size 13698 size 13698

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:42385da2eb74d54ba086aed973ade15f2a8d2be0c9281c05e6fb88846137bf81 oid sha256:2e8e03c2a42e195e6489659053aecb78755d3c218558cb2e9339fa7b6db59405
size 35870 size 35875

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:9f1b6fa0c48479606539f2d98befe1c9ee881846c0b55d7a53313962d556380d oid sha256:ad22ea6b6e69fd71416fdae76cbd142d279f8f562e74b77e63b3989be187c57c
size 484629 size 484631

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:89074b8dab103a419bc3dac743da4d8c47f435fa55b98d8aab71f6c9fb4d39de oid sha256:c8ea98c65376d9f6ac66d0a9471c4bf3add0904294e7ca1a105458b90654a2e2
size 12370 size 12476

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:7bd7b54ff60859e4d4793000bef3adbec4c071063bec6bfdbde62516c4fc3478 oid sha256:3793a5e83ef9bdffef99bcd8905a094acb69cde356e3a7125a544045296c3926
size 12959 size 13070

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:b53b03212953e12915a0e41bff5f0cdea90f8f866220a01142edaeb915735a34 oid sha256:941582e2e20a9459db1f2cb7f07fa1930acfdb12cbbe7f96f9aafbeabf8b37f6
size 47077 size 47076

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:22e5d61a141b5a8663feb8a47371f9259d2a77fdacb1245bce411ffc85ce2cae oid sha256:2735a021f171f5c95888cda76e8668e1e023588c8c6c7cd382c03d8e31988fe3
size 47716 size 48209

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:120558ab0c267650744bd078aeace8d4122b3569c5998602f969766131d15c44 oid sha256:867bef6b55b73d127306a461e115b6f0047d582904999de80aeabae00e60c967
size 43894 size 44295

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:af92548b6c8569081a91cb772b73988d9cb342498ddf9c0c86b6963cef8eda9e oid sha256:936ec8b223ae7f0f32c640c127e1b6b14033bb7d168a4d1f0e6b3bd08a761e36
size 43985 size 44055

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:db510af76578693c85ce78ca91224758a56f7bbf33db3221c9a4edca08b06600 oid sha256:fba7387f5deba5e144e2106154b15ab956a50a418857bd34e16b306d7f1a29e4
size 590547 size 588252

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:cae2b789e8afff23b7545d42a530e6c972d28736bad2bdacbc69f0e7065f85cc oid sha256:4656f3255d7859c07b269ff655eafe21bdddb949a07aa91477b826f6e2af8c28
size 740660 size 740616

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:09d9f567ec371d60881b525ddb462d9135552db97af5921a6eb02aba40e40616 oid sha256:b18ff644ba5bd0c7f094bf8eac079d8a72bc6918638b1b110002f2f0a7a362cc
size 971544 size 967860

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:3c383dd89fda6094704027074a72085591339a276d60502626d78e8e527b2e10 oid sha256:134caff5b8a4969055c32e8f51ca9c6eae1528b84d348691d860913e839de0d9
size 1076719 size 1076746

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:0b4559541cf3259496c760a26f8d83e82179cb7e4576333682c5af49ee4a35a7 oid sha256:d731b4ce039315e096113f3c83168165020949e57564e641e778728e35901169
size 1125331 size 1125286

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:67c8412a1e8fdbfd88f8573797fbf6fbd89c6ce783a074a8e90f7d8d9e67dd57 oid sha256:cfac3518220555984d47c9fdfea2202a37102250aefcc2509794f337b3a7baae
size 1366351 size 1361407

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:a2b7b54a1af0f5cd31bd64f0506e3035dd423314ce3389e61730fa160434fbf3 oid sha256:cf21fe763e9762bca1b0f486e29a6024efcbc106a7f1ac195104acd0621cf8db
size 45074 size 45107

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:7b66a0be67ff2d684a54c2321123521b3ad06dfe5ebffd50e89260d77efcfcc4 oid sha256:2f09338e652b965cc9ae7bbb261845cd9c15d79f3d15f3c5b5326ef6d163b606
size 86833 size 86885

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:19320291c99a23429b114a59de4636689e281e1e68766abe2aa1e56562128e50 oid sha256:e298244953653e46875053b12b4fe06ee692cb58fc131233ac4172677f0f8b44
size 118919 size 118961

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:5edf089c00715f1456fe7838e85aadcfc42b6216a3fd95b48d9c21fc8d700cba oid sha256:6b9b36acf821cca71f97a3c8468fb925561f3bc2030742aef1e3c1d9e69ccc6f
size 51371 size 51419

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:6cd1a10639dcb323bdc3b2c43e0c35665184fc809731ced90088ee9edb9de845 oid sha256:f5ad7a37546d48fc5426c32534a1c452fd0bf8280346dbe6e67ac26f17f3ba8a
size 54577 size 54626

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:87e34024f701dc93f4026213ac7eb468a2cd6d3393eb0dbec382bf58007f8e61 oid sha256:c0b61e9d1c2bcbf891a7acd4f3c1d2bd7524133d8165e7e7984998670de5a085
size 55042 size 55090

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d7940ff56796efb27bec66b632ff33aa2ad390c4962a711bf520aee341f035a4 oid sha256:a2e4975e9328a6d72f2c932daddfbb00cebdb2249aceb53f667d4060a1c0ea8a
size 35968 size 36006

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:b7bbd16c8aad444f0d11aacf87cf2292d494cc80a1ca46e7e8db86ca3041d35a oid sha256:ac6f9adeef92be9f69cb288ccafda8d522b8c3cde64352cd5369ae63668240c0
size 35931 size 35973

View File

@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0475c5ac04ab8f79b79d43cfdb985f05b61dbe90e81f898a6dc216c308a28841
size 4707

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:344d90928510855dc718a2e36e31a97f084f1163ab750d0217fb8620469b621a
size 5276

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:60449af267336663304e44e254d0984e037bebfa2d1efdf32234cab4374e8c79
size 5301

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:bbdc4199dee2ae853b8a240cd84528482dc6762233bd0d1249f2daa296b49487 oid sha256:c5a45307147f19f2d69a3de1f53e0a73ba4c3368eb25a66b4098fb54cb83822f
size 64172 size 64203

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:f6d38b6b47839d0e4eae530d203c83971fba8a41c9caa3d5b5d89ee7ed582613 oid sha256:0102aa84db99a6da1db1de3abf67f13c3b571de00e79e7c55805dc0504658d50
size 150090 size 150111

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:c0635f1564d6c9707efa68003fb8c9b6eb00408aa8f24c972e33c6c79fed5bdf oid sha256:3991cb1f922e0c6712d045b3cd8a1d98165c0fbef7e31b15d587f244e53ec04a
size 59354 size 59343

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:4288ee4a0d2229d59c31538179cdda50035a3849f69b400127e1618efe30cdc1 oid sha256:355d8f08d08011635bf812aea1edeabd69e1ac3c724b521ed243f2b52e9b444b
size 145224 size 145257

View File

@@ -38,7 +38,7 @@ egui.workspace = true
eframe = { workspace = true, optional = true } eframe = { workspace = true, optional = true }
kittest.workspace = true kittest.workspace = true
serde.workspace = true serde.workspace = true
toml.workspace = true toml = {workspace = true, features = ["parse", "serde"] }
# wgpu dependencies # wgpu dependencies
egui-wgpu = { workspace = true, optional = true } egui-wgpu = { workspace = true, optional = true }

View File

@@ -97,12 +97,12 @@ You should add the following to your `.gitignore`:
* …have a low resolution to avoid growth in repo size * …have a low resolution to avoid growth in repo size
* …have a low comparison threshold to avoid the test passing despite unwanted differences (the default threshold should be fine for most usecases!) * …have a low comparison threshold to avoid the test passing despite unwanted differences (the default threshold should be fine for most usecases!)
### What do do when CI / another computer produces a different image? ### What to do when CI / another computer produces a different image?
The default tolerance settings should be fine for almost all gui comparison tests. The default tolerance settings should be fine for almost all gui comparison tests.
However, especially when you're using custom rendering, you may observe images difference with different setups leading to unexpected test failures. However, especially when you're using custom rendering, you may observe images difference with different setups leading to unexpected test failures.
First check whether the difference is due to a change in enabled rendering features, potentially due to difference in hardware (/software renderer) capabilitites. First check whether the difference is due to a change in enabled rendering features, potentially due to difference in hardware (/software renderer) capabilities.
Generally you should carefully enforcing the same set of features for all test runs, but this may happen nonetheless. Generally you should carefully enforcing the same set of features for all test runs, but this may happen nonetheless.
Once you validated that the differences are miniscule and hard to avoid, you can try to _carefully_ adjust the comparison tolerance setting (`SnapshotOptions::threshold`, TODO([#5683](https://github.com/emilk/egui/issues/5683)): as well as number of pixels allowed to differ) for the specific test. Once you validated that the differences are miniscule and hard to avoid, you can try to _carefully_ adjust the comparison tolerance setting (`SnapshotOptions::threshold`, TODO([#5683](https://github.com/emilk/egui/issues/5683)): as well as number of pixels allowed to differ) for the specific test.

View File

@@ -98,9 +98,11 @@ impl Node<'_> {
/// This will trigger a [`accesskit::Action::Click`] action. /// This will trigger a [`accesskit::Action::Click`] action.
/// In contrast to `click()`, this can also click widgets that are not currently visible. /// In contrast to `click()`, this can also click widgets that are not currently visible.
pub fn click_accesskit(&self) { pub fn click_accesskit(&self) {
let (target_node, target_tree) = self.accesskit_node.locate();
self.event(egui::Event::AccessKitActionRequest( self.event(egui::Event::AccessKitActionRequest(
accesskit::ActionRequest { accesskit::ActionRequest {
target: self.accesskit_node.id(), target_node,
target_tree,
action: accesskit::Action::Click, action: accesskit::Action::Click,
data: None, data: None,
}, },
@@ -119,9 +121,11 @@ impl Node<'_> {
} }
pub fn focus(&self) { pub fn focus(&self) {
let (target_node, target_tree) = self.accesskit_node.locate();
self.event(egui::Event::AccessKitActionRequest(ActionRequest { self.event(egui::Event::AccessKitActionRequest(ActionRequest {
action: accesskit::Action::Focus, action: accesskit::Action::Focus,
target: self.accesskit_node.id(), target_node,
target_tree,
data: None, data: None,
})); }));
} }
@@ -162,45 +166,55 @@ impl Node<'_> {
/// Scroll the node into view. /// Scroll the node into view.
pub fn scroll_to_me(&self) { pub fn scroll_to_me(&self) {
let (target_node, target_tree) = self.accesskit_node.locate();
self.event(egui::Event::AccessKitActionRequest(ActionRequest { self.event(egui::Event::AccessKitActionRequest(ActionRequest {
action: accesskit::Action::ScrollIntoView, action: accesskit::Action::ScrollIntoView,
target: self.accesskit_node.id(), target_node,
target_tree,
data: None, data: None,
})); }));
} }
/// Scroll the [`egui::ScrollArea`] containing this node down (100px). /// Scroll the [`egui::ScrollArea`] containing this node down (100px).
pub fn scroll_down(&self) { pub fn scroll_down(&self) {
let (target_node, target_tree) = self.accesskit_node.locate();
self.event(egui::Event::AccessKitActionRequest(ActionRequest { self.event(egui::Event::AccessKitActionRequest(ActionRequest {
action: accesskit::Action::ScrollDown, action: accesskit::Action::ScrollDown,
target: self.accesskit_node.id(), target_node,
target_tree,
data: None, data: None,
})); }));
} }
/// Scroll the [`egui::ScrollArea`] containing this node up (100px). /// Scroll the [`egui::ScrollArea`] containing this node up (100px).
pub fn scroll_up(&self) { pub fn scroll_up(&self) {
let (target_node, target_tree) = self.accesskit_node.locate();
self.event(egui::Event::AccessKitActionRequest(ActionRequest { self.event(egui::Event::AccessKitActionRequest(ActionRequest {
action: accesskit::Action::ScrollUp, action: accesskit::Action::ScrollUp,
target: self.accesskit_node.id(), target_node,
target_tree,
data: None, data: None,
})); }));
} }
/// Scroll the [`egui::ScrollArea`] containing this node left (100px). /// Scroll the [`egui::ScrollArea`] containing this node left (100px).
pub fn scroll_left(&self) { pub fn scroll_left(&self) {
let (target_node, target_tree) = self.accesskit_node.locate();
self.event(egui::Event::AccessKitActionRequest(ActionRequest { self.event(egui::Event::AccessKitActionRequest(ActionRequest {
action: accesskit::Action::ScrollLeft, action: accesskit::Action::ScrollLeft,
target: self.accesskit_node.id(), target_node,
target_tree,
data: None, data: None,
})); }));
} }
/// Scroll the [`egui::ScrollArea`] containing this node right (100px). /// Scroll the [`egui::ScrollArea`] containing this node right (100px).
pub fn scroll_right(&self) { pub fn scroll_right(&self) {
let (target_node, target_tree) = self.accesskit_node.locate();
self.event(egui::Event::AccessKitActionRequest(ActionRequest { self.event(egui::Event::AccessKitActionRequest(ActionRequest {
action: accesskit::Action::ScrollRight, action: accesskit::Action::ScrollRight,
target: self.accesskit_node.id(), target_node,
target_tree,
data: None, data: None,
})); }));
} }

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:3ca39801faddae7191ed054029263e8eca488d16e1fcbb40fed482d39fc89e8e oid sha256:00fb02e0cc2c1454d3a3dc0635be24086234c2bc5e2c9fd73741b179622e16d6
size 4520 size 4514

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:bafe5d7129cd2137b8f7bc9662b894d959b7042c436443f835ecd421a0d9c33f oid sha256:d8757e2db9a3892d9347495ad59f14d2bd9164a9ba258375a53c9faf8176b597
size 8019 size 8016

Some files were not shown because too many files have changed in this diff Show More