1
0
mirror of https://github.com/emilk/egui.git synced 2026-06-26 22:53:14 -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
ro = "ro" # read-only, also part of the username @Phen-Ro
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
tesalator = "tessellator"
@@ -20,6 +21,8 @@ teselation = "tessellation"
tessalation = "tessellation"
tesselation = "tessellation"
# For consistency
postfix = "suffix"
# Use the more common spelling
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 }
eframe = { version = "0.33.3", path = "crates/eframe", default-features = false }
accesskit = "0.21.1"
accesskit_consumer = "0.30.1"
accesskit_winit = "0.29.1"
accesskit = "0.24.0"
accesskit_consumer = "0.35.0"
accesskit_winit = "0.32.0"
ahash = { version = "0.8.12", default-features = false, features = [
"no-rng", # we don't need DOS-protection, so we let users opt-in to it instead
"std",
@@ -84,12 +84,13 @@ chrono = { version = "0.4.42", default-features = false }
cint = "0.3.1"
color-hex = "0.2.0"
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"
document-features = "0.2.11"
ehttp = { version = "0.6.0", default-features = false }
enum-map = "2.7.3"
env_logger = { version = "0.11.8", default-features = false }
font-types = { version = "0.11.0", default-features = false, features = ["std"] }
glow = "0.16.0"
glutin = { version = "0.32.3", 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"
rayon = "1.11.0"
resvg = { version = "0.45.1", default-features = false }
rfd = "0.15.4"
rfd = "0.17.2"
ron = "0.11.0"
self_cell = "1.2.1"
serde = { version = "1.0.228", features = ["derive"] }
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"
smithay-clipboard = "0.7.2"
static_assertions = "1.1.0"
syntect = { version = "5.3.0", default-features = false }
tempfile = "3.23.0"
thiserror = "2.0.17"
tokio = "1.47.1"
toml = "0.8"
tokio = "1.49"
toml = {version = "1", default-features = false }
type-map = "0.5.1"
unicode_names2 = { version = "2.0.0", default-features = false }
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-futures = "0.4.0"
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"
winit = { version = "0.30.12", default-features = false }
[patch.crates-io]
kittest = { git = "https://github.com/rerun-io/kittest", branch = "main" }
[workspace.lints.rust]
unsafe_code = "deny"

View File

@@ -72,7 +72,8 @@ pub struct CreationContext<'s> {
/// The `get_proc_address` wrapper of underlying GL context
#[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.
///

View File

@@ -265,6 +265,7 @@ impl EpiIntegration {
app: &mut dyn epi::App,
viewport_ui_cb: Option<&DeferredViewportUiCallback>,
mut raw_input: egui::RawInput,
is_visible: bool,
) -> egui::FullOutput {
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| {
if let Some(viewport_ui_cb) = viewport_ui_cb {
// Child viewport
profiling::scope!("viewport_callback");
viewport_ui_cb(ui);
if is_visible {
profiling::scope!("viewport_callback");
viewport_ui_cb(ui);
}
} else {
{
profiling::scope!("App::logic");
app.logic(ui.ctx(), &mut self.frame);
}
{
profiling::scope!("App::update");
#[expect(deprecated)]
app.update(ui.ctx(), &mut self.frame);
}
if is_visible {
{
profiling::scope!("App::update");
#[expect(deprecated)]
app.update(ui.ctx(), &mut self.frame);
}
{
profiling::scope!("App::ui");
app.ui(ui, &mut self.frame);
{
profiling::scope!("App::ui");
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 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 cc = CreationContext {
egui_ctx: integration.egui_ctx.clone(),
integration_info: integration.frame.info().clone(),
storage: integration.frame.storage(),
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")]
wgpu_render_state: None,
raw_display_handle: window.display_handle().map(|h| h.as_raw()),
@@ -447,12 +448,21 @@ impl WinitApp for GlowWinitApp<'_> {
if let Some(viewport) = glutin
.focused_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() {
egui_winit.on_mouse_motion(delta);
if !window.has_focus()
&& !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()));
}
}
@@ -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 egui_ctx = glutin.egui_ctx.clone();
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);
let is_visible = viewport.info.visible().unwrap_or(true);
let Some(egui_winit) = viewport.egui_winit.as_mut() else {
return Ok(EventResult::Wait);
};
@@ -561,7 +573,7 @@ impl GlowWinitRunning<'_> {
.map(|(id, viewport)| (*id, viewport.info.clone()))
.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
@@ -577,7 +589,7 @@ impl GlowWinitRunning<'_> {
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.
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:
let mut glutin = self.glutin.borrow_mut();
@@ -612,9 +624,12 @@ impl GlowWinitRunning<'_> {
// The update function, which could call immediate viewports,
// so make sure we don't hold any locks here required by the immediate viewports rendeer.
let full_output =
self.integration
.update(self.app.as_mut(), viewport_ui_cb.as_deref(), raw_input);
let full_output = self.integration.update(
self.app.as_mut(),
viewport_ui_cb.as_deref(),
raw_input,
is_visible,
);
// ------------------------------------------------------------
@@ -657,85 +672,87 @@ impl GlowWinitRunning<'_> {
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:
frame_timer.pause();
change_gl_context(current_gl_context, not_current_gl_context, gl_surface);
frame_timer.resume();
}
{
// We may need to switch contexts again, because of immediate viewports:
frame_timer.pause();
change_gl_context(current_gl_context, not_current_gl_context, gl_surface);
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 {
painter.clear(screen_size_in_pixels, clear_color);
}
if !clear_before_update {
painter.clear(screen_size_in_pixels, clear_color);
}
painter.paint_and_update_textures(
screen_size_in_pixels,
pixels_per_point,
&clipped_primitives,
&textures_delta,
);
painter.paint_and_update_textures(
screen_size_in_pixels,
pixels_per_point,
&clipped_primitives,
&textures_delta,
);
{
for action in viewport.actions_requested.drain(..) {
match action {
ActionRequested::Screenshot(user_data) => {
let screenshot = painter.read_screen_rgba(screen_size_in_pixels);
egui_winit
.egui_input_mut()
.events
.push(egui::Event::Screenshot {
viewport_id,
user_data,
image: screenshot.into(),
});
}
ActionRequested::Cut => {
egui_winit.egui_input_mut().events.push(egui::Event::Cut);
}
ActionRequested::Copy => {
egui_winit.egui_input_mut().events.push(egui::Event::Copy);
}
ActionRequested::Paste => {
if let Some(contents) = egui_winit.clipboard_text() {
let contents = contents.replace("\r\n", "\n");
if !contents.is_empty() {
egui_winit
.egui_input_mut()
.events
.push(egui::Event::Paste(contents));
{
for action in viewport.actions_requested.drain(..) {
match action {
ActionRequested::Screenshot(user_data) => {
let screenshot = painter.read_screen_rgba(screen_size_in_pixels);
egui_winit
.egui_input_mut()
.events
.push(egui::Event::Screenshot {
viewport_id,
user_data,
image: screenshot.into(),
});
}
ActionRequested::Cut => {
egui_winit.egui_input_mut().events.push(egui::Event::Cut);
}
ActionRequested::Copy => {
egui_winit.egui_input_mut().events.push(egui::Event::Copy);
}
ActionRequested::Paste => {
if let Some(contents) = egui_winit.clipboard_text() {
let contents = contents.replace("\r\n", "\n");
if !contents.is_empty() {
egui_winit
.egui_input_mut()
.events
.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(),
)
})?;
{
// 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)?;
frame_timer.resume();
}
gl_surface.swap_buffers(context)?;
frame_timer.resume();
}
// give it time to settle:
#[cfg(feature = "__screenshot")]
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);
// give it time to settle:
#[cfg(feature = "__screenshot")]
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);
@@ -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 => {
if viewport_id == Some(ViewportId::ROOT) && self.integration.should_close() {
log::debug!(

View File

@@ -454,12 +454,21 @@ impl WinitApp for WgpuWinitApp<'_> {
if let Some(viewport) = shared
.focused_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() {
egui_winit.on_mouse_motion(delta);
if !window.has_focus()
&& !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()));
}
}
@@ -564,7 +573,7 @@ impl WgpuWinitRunning<'_> {
let mut frame_timer = crate::stopwatch::Stopwatch::new();
frame_timer.start();
let (viewport_ui_cb, raw_input) = {
let (viewport_ui_cb, raw_input, is_visible) = {
profiling::scope!("Prepare");
let mut shared_lock = shared.borrow_mut();
@@ -608,6 +617,8 @@ impl WgpuWinitRunning<'_> {
};
egui_winit::update_viewport_info(info, &integration.egui_ctx, window, false);
let is_visible = viewport.info.visible().unwrap_or(true);
{
profiling::scope!("set_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);
(viewport_ui_cb, raw_input)
(viewport_ui_cb, raw_input, is_visible)
};
// ------------------------------------------------------------
// Runs the update, which could call immediate viewports,
// 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);
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![];
viewport.actions_requested.retain(|cmd| {
if let ActionRequested::Screenshot(info) = cmd {
screenshot_commands.push(info.clone());
false
} else {
true
}
});
let vsync_secs = painter.paint_and_update_textures(
viewport_id,
pixels_per_point,
app.clear_color(&egui_ctx.global_style().visuals),
&clipped_primitives,
&textures_delta,
screenshot_commands,
);
let mut screenshot_commands = vec![];
viewport.actions_requested.retain(|cmd| {
if let ActionRequested::Screenshot(info) = cmd {
screenshot_commands.push(info.clone());
false
} else {
true
}
});
let vsync_secs = painter.paint_and_update_textures(
viewport_id,
pixels_per_point,
app.clear_color(&egui_ctx.global_style().visuals),
&clipped_primitives,
&textures_delta,
screenshot_commands,
);
for action in viewport.actions_requested.drain(..) {
match action {
ActionRequested::Screenshot { .. } => {
// already handled above
}
ActionRequested::Cut => {
egui_winit.egui_input_mut().events.push(egui::Event::Cut);
}
ActionRequested::Copy => {
egui_winit.egui_input_mut().events.push(egui::Event::Copy);
}
ActionRequested::Paste => {
if let Some(contents) = egui_winit.clipboard_text() {
let contents = contents.replace("\r\n", "\n");
if !contents.is_empty() {
egui_winit
.egui_input_mut()
.events
.push(egui::Event::Paste(contents));
for action in viewport.actions_requested.drain(..) {
match action {
ActionRequested::Screenshot { .. } => {
// already handled above
}
ActionRequested::Cut => {
egui_winit.egui_input_mut().events.push(egui::Event::Cut);
}
ActionRequested::Copy => {
egui_winit.egui_input_mut().events.push(egui::Event::Copy);
}
ActionRequested::Paste => {
if let Some(contents) = egui_winit.clipboard_text() {
let contents = contents.replace("\r\n", "\n");
if !contents.is_empty() {
egui_winit
.egui_input_mut()
.events
.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();
@@ -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 => {
if viewport_id == Some(ViewportId::ROOT) && integration.should_close() {
log::debug!(

View File

@@ -274,13 +274,21 @@ impl AppRunner {
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| {
self.app.logic(ui.ctx(), &mut self.frame);
#[expect(deprecated)]
self.app.update(ui.ctx(), &mut self.frame);
if is_visible {
#[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 {
platform_output,
@@ -311,8 +319,10 @@ impl AppRunner {
}
self.handle_platform_output(platform_output);
self.textures_delta.append(textures_delta);
self.clipped_primitives = Some(self.egui_ctx.tessellate(shapes, pixels_per_point));
if is_visible {
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`].

View File

@@ -31,11 +31,18 @@ impl WebInput {
time: Some(super::now_sec()),
..self.raw.take()
};
raw_input
let viewport = raw_input
.viewports
.entry(egui::ViewportId::ROOT)
.or_default()
.native_pixels_per_point = Some(super::native_pixels_per_point());
.or_default();
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
}

View File

@@ -420,12 +420,40 @@ impl Painter {
) -> f32 {
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 mut vsync_sec = 0.0;
let Some(render_state) = self.render_state.as_mut() else {
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 {
return vsync_sec;
};
@@ -590,6 +618,9 @@ impl Painter {
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.
// 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.

View File

@@ -102,7 +102,7 @@ pub struct State {
has_sent_ime_enabled: bool,
#[cfg(feature = "accesskit")]
accesskit: Option<accesskit_winit::Adapter>,
pub accesskit: Option<accesskit_winit::Adapter>,
allow_ime: bool,
ime_rect_px: Option<egui::Rect>,
@@ -640,11 +640,27 @@ impl State {
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 {
x: delta.0 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`].

View File

@@ -1,5 +1,5 @@
use crate::{AtomKind, FontSelection, Id, SizedAtom, Ui};
use emath::{NumExt as _, Vec2};
use crate::{AtomKind, FontSelection, Id, IntoSizedArgs, IntoSizedResult, SizedAtom, Ui};
use emath::{Align2, NumExt as _, Vec2};
use epaint::text::TextWrapMode;
/// A low-level ui building block.
@@ -14,6 +14,9 @@ use epaint::text::TextWrapMode;
/// ```
#[derive(Clone, Debug)]
pub struct Atom<'a> {
/// See [`crate::AtomExt::atom_id`]
pub id: Option<Id>,
/// See [`crate::AtomExt::atom_size`]
pub size: Option<Vec2>,
@@ -26,17 +29,22 @@ pub struct Atom<'a> {
/// See [`crate::AtomExt::atom_shrink`]
pub shrink: bool,
/// The atom type
/// See [`crate::AtomExt::atom_align`]
pub align: Align2,
/// The atom type / content
pub kind: AtomKind<'a>,
}
impl Default for Atom<'_> {
fn default() -> Self {
Atom {
id: None,
size: None,
max_size: Vec2::INFINITY,
grow: false,
shrink: false,
align: Align2::CENTER_CENTER,
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 {
Atom {
size: Some(size.into()),
kind: AtomKind::Custom(id),
kind: AtomKind::Empty,
id: Some(id),
..Default::default()
}
}
@@ -82,19 +106,32 @@ impl<'a> Atom<'a> {
wrap_mode = Some(TextWrapMode::Truncate);
}
let (intrinsic, kind) = self
.kind
.into_sized(ui, available_size, wrap_mode, fallback_font);
let id = self.id;
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
.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 {
id,
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,
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;
/// A trait for conveniently building [`Atom`]s.
///
/// The functions are prefixed with `atom_` to avoid conflicts with e.g. [`crate::RichText::size`].
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.
///
/// 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));
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
where
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> {
let mut atom = self.into();
atom.size = Some(size);
@@ -104,4 +121,10 @@ where
atom.max_size.y = max_height;
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 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.
#[derive(Clone, Default, Debug)]
#[derive(Default)]
pub enum AtomKind<'a> {
/// Empty, that can be used with [`crate::AtomExt::atom_grow`] to reserve space.
#[default]
@@ -38,37 +57,57 @@ pub enum AtomKind<'a> {
/// default font height, which is convenient for icons.
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
/// [`crate::Painter`] or [`Ui::place`] to add/draw some custom content.
/// The vec2 passed in is the available size to this atom. The returned vec2 should be the
/// preferred / intrinsic 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("⏵"));
/// }
/// # });
/// ```
Custom(Id),
/// Note: This api is experimental, expect breaking changes here.
/// When cloning, this will be cloned as [`AtomKind::Empty`].
Closure(AtomClosure<'a>),
}
impl Clone for AtomKind<'_> {
fn clone(&self) -> Self {
match self {
AtomKind::Empty => AtomKind::Empty,
AtomKind::Text(text) => AtomKind::Text(text.clone()),
AtomKind::Image(image) => AtomKind::Image(image.clone()),
AtomKind::Closure(_) => {
log::warn!("Cannot clone atom closures");
AtomKind::Empty
}
}
}
}
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> {
/// See [`Self::Text`]
pub fn text(text: impl Into<WidgetText>) -> Self {
AtomKind::Text(text.into())
}
/// See [`Self::Image`]
pub fn image(image: impl Into<Image<'a>>) -> Self {
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`].
///
/// 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(
self,
ui: &Ui,
available_size: Vec2,
wrap_mode: Option<TextWrapMode>,
fallback_font: FontSelection,
) -> (Vec2, SizedAtomKind<'a>) {
IntoSizedArgs {
available_size,
wrap_mode,
fallback_font,
}: IntoSizedArgs,
) -> IntoSizedResult<'a> {
match self {
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);
(galley.intrinsic_size(), SizedAtomKind::Text(galley))
IntoSizedResult {
intrinsic_size: galley.intrinsic_size(),
sized: SizedAtomKind::Text(galley),
}
}
AtomKind::Image(image) => {
let size = image.load_and_calc_size(ui, available_size);
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 => (Vec2::ZERO, SizedAtomKind::Empty),
AtomKind::Empty => IntoSizedResult {
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_font: Option<FontSelection>,
min_size: Vec2,
max_size: Vec2,
wrap_mode: Option<TextWrapMode>,
align2: Option<Align2>,
}
@@ -59,6 +60,7 @@ impl<'a> AtomLayout<'a> {
fallback_text_color: None,
fallback_font: None,
min_size: Vec2::ZERO,
max_size: Vec2::INFINITY,
wrap_mode: None,
align2: None,
}
@@ -113,6 +115,33 @@ impl<'a> AtomLayout<'a> {
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`].
#[inline]
pub fn id(mut self, id: Id) -> Self {
@@ -161,6 +190,7 @@ impl<'a> AtomLayout<'a> {
sense,
fallback_text_color,
min_size,
mut max_size,
wrap_mode,
align2,
fallback_font,
@@ -190,8 +220,16 @@ impl<'a> AtomLayout<'a> {
fallback_text_color.unwrap_or_else(|| ui.style().visuals.text_color());
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
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;
@@ -321,7 +359,7 @@ impl<'atom> AllocatedAtomLayout<'atom> {
pub fn iter_images(&self) -> impl Iterator<Item = &Image<'atom>> {
self.iter_kinds().filter_map(|kind| {
if let SizedAtomKind::Image(image, _) = kind {
if let SizedAtomKind::Image { image, size: _ } = kind {
Some(image)
} else {
None
@@ -331,7 +369,7 @@ impl<'atom> AllocatedAtomLayout<'atom> {
pub fn iter_images_mut(&mut self) -> impl Iterator<Item = &mut Image<'atom>> {
self.iter_kinds_mut().filter_map(|kind| {
if let SizedAtomKind::Image(image, _) = kind {
if let SizedAtomKind::Image { image, size: _ } = kind {
Some(image)
} else {
None
@@ -373,8 +411,11 @@ impl<'atom> AllocatedAtomLayout<'atom> {
F: FnMut(Image<'atom>) -> Image<'atom>,
{
self.map_kind(|kind| {
if let SizedAtomKind::Image(image, size) = kind {
SizedAtomKind::Image(f(image), size)
if let SizedAtomKind::Image { image, size } = kind {
SizedAtomKind::Image {
image: f(image),
size,
}
} else {
kind
}
@@ -422,25 +463,24 @@ impl<'atom> AllocatedAtomLayout<'atom> {
.with_min_x(cursor)
.with_max_x(cursor + size.x + growth);
cursor = frame.right() + gap;
let rect = sized.align.align_size_within_rect(size, frame);
let align = Align2::CENTER_CENTER;
let rect = align.align_size_within_rect(size, frame);
if let Some(id) = sized.id {
debug_assert!(
!response.custom_rects.iter().any(|(i, _)| *i == id),
"Duplicate custom id"
);
response.custom_rects.push((id, rect));
}
match sized.kind {
SizedAtomKind::Text(galley) => {
ui.painter().galley(rect.min, galley, fallback_text_color);
}
SizedAtomKind::Image(image, _) => {
SizedAtomKind::Image { image, size: _ } => {
image.paint_at(ui, rect);
}
SizedAtomKind::Custom(id) => {
debug_assert!(
!response.custom_rects.iter().any(|(i, _)| *i == id),
"Duplicate custom id"
);
response.custom_rects.push((id, rect));
}
SizedAtomKind::Empty => {}
SizedAtomKind::Empty { .. } => {}
}
}
@@ -450,7 +490,7 @@ impl<'atom> AllocatedAtomLayout<'atom> {
/// 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)]
pub struct AtomLayoutResponse {
pub response: Response,
@@ -470,7 +510,7 @@ impl AtomLayoutResponse {
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.
pub fn rect(&self, id: Id) -> Option<Rect> {

View File

@@ -21,11 +21,26 @@ impl<'a> Atoms<'a> {
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).
pub fn push_left(&mut self, atom: impl Into<Atom<'a>>) {
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.
// 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.

View File

@@ -4,6 +4,8 @@ use emath::Vec2;
/// A [`crate::Atom`] which has been sized.
#[derive(Clone, Debug)]
pub struct SizedAtom<'a> {
pub id: Option<crate::Id>,
pub(crate) grow: bool,
/// 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`.
pub intrinsic_size: Vec2,
/// How will the atom be aligned in its available space?
pub align: emath::Align2,
pub kind: SizedAtomKind<'a>,
}

View File

@@ -1,16 +1,20 @@
use crate::{Id, Image};
use crate::Image;
use emath::Vec2;
use epaint::Galley;
use std::sync::Arc;
/// A sized [`crate::AtomKind`].
#[derive(Clone, Default, Debug)]
#[derive(Clone, Debug)]
pub enum SizedAtomKind<'a> {
#[default]
Empty,
Empty { size: Option<Vec2> },
Text(Arc<Galley>),
Image(Image<'a>, Vec2),
Custom(Id),
Image { image: Image<'a>, size: Vec2 },
}
impl Default for SizedAtomKind<'_> {
fn default() -> Self {
Self::Empty { size: None }
}
}
impl SizedAtomKind<'_> {
@@ -18,8 +22,8 @@ impl SizedAtomKind<'_> {
pub fn size(&self) -> Vec2 {
match self {
SizedAtomKind::Text(galley) => galley.size(),
SizedAtomKind::Image(_, size) => *size,
SizedAtomKind::Empty | SizedAtomKind::Custom(_) => Vec2::ZERO,
SizedAtomKind::Image { image: _, size } => *size,
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:
ui.input_mut(|input| {
if always_scroll_enabled_direction {
input.smooth_scroll_delta()[0] = 0.0;
input.smooth_scroll_delta()[1] = 0.0;
input.smooth_scroll_delta = Vec2::ZERO;
} 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 {
nodes,
tree: Some(accesskit::Tree::new(root_id)),
tree_id: accesskit::TreeId::ROOT,
focus: focus_id,
});
}
@@ -3262,7 +3263,7 @@ impl Context {
for (name, data) in &mut font_definitions.font_data {
ui.collapsing(name, |ui| {
let mut tweak = data.tweak;
let mut tweak = data.tweak.clone();
if tweak.ui(ui).changed() {
Arc::make_mut(data).tweak = tweak;
changed = true;

View File

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

View File

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

View File

@@ -661,6 +661,8 @@ impl InputState {
if self.pointer.wants_repaint()
|| self.wheel.unprocessed_wheel_delta.abs().max_elem() > 0.2
|| !self.events.is_empty()
|| !self.raw.hovered_files.is_empty()
|| !self.raw.dropped_files.is_empty()
{
// Immediate repaint
return Some(Duration::ZERO);
@@ -869,7 +871,8 @@ impl InputState {
let accesskit_id = id.accesskit_id();
self.events.iter().filter_map(move |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
{
return Some(request);
@@ -886,7 +889,8 @@ impl InputState {
let accesskit_id = id.accesskit_id();
self.events.retain(|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);
}

View File

@@ -685,7 +685,7 @@ pub fn __run_test_ctx(mut run_ui: impl FnMut(&Context)) {
}
/// 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();
ctx.set_fonts(FontDefinitions::empty()); // prevent fonts from being loaded (save CPU time)
let _ = ctx.run_ui(Default::default(), |ui| {

View File

@@ -564,11 +564,13 @@ impl Focus {
if let crate::Event::AccessKitActionRequest(accesskit::ActionRequest {
action: accesskit::Action::Focus,
target,
target_node,
target_tree,
data: None,
}) = 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).
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 crate::{
@@ -2837,7 +2841,7 @@ impl Widget for &mut crate::Frame {
impl Widget for &mut FontTweak {
fn ui(self, ui: &mut Ui) -> Response {
let original: FontTweak = *self;
let original: FontTweak = self.clone();
let mut response = Grid::new("font_tweak")
.num_columns(2)
@@ -2847,6 +2851,7 @@ impl Widget for &mut FontTweak {
y_offset_factor,
y_offset,
hinting_override,
coords,
} = self;
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(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() {
*self = Default::default();

View File

@@ -4,6 +4,26 @@ use crate::{Context, Galley, Id};
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.
pub fn update_accesskit_for_text_widget(
ctx: &Context,
@@ -20,14 +40,8 @@ pub fn update_accesskit_for_text_widget(
let anchor = galley.layout_from_cursor(cursor_range.secondary);
let focus = galley.layout_from_cursor(cursor_range.primary);
builder.set_text_selection(accesskit::TextSelection {
anchor: accesskit::TextPosition {
node: parent_id.with(anchor.row).accesskit_id(),
character_index: anchor.column,
},
focus: accesskit::TextPosition {
node: parent_id.with(focus.row).accesskit_id(),
character_index: focus.column,
},
anchor: text_run_position(parent_id, anchor.row, anchor.column),
focus: text_run_position(parent_id, focus.row, focus.column),
});
}
@@ -40,61 +54,144 @@ pub fn update_accesskit_for_text_widget(
return;
};
let mut prev_row_ended_with_newline = true;
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| {
builder.set_role(accesskit::Role::TextRun);
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_text_direction(accesskit::TextDirection::LeftToRight);
// TODO(mwcampbell): Set more node fields for the row
// once AccessKit adapters expose text formatting info.
if row.ends_with_newline {
value.push('\n');
character_lengths.push(1);
character_positions.push(row.size.x);
character_widths.push(0.0);
}
let glyph_count = row.glyphs.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;
let total_chars = character_lengths.len();
for glyph in &row.glyphs {
let is_word_char = is_word_char(glyph.chr);
if is_word_char && was_at_word_end {
word_lengths.push((character_lengths.len() - last_word_start) as _);
last_word_start = character_lengths.len();
if total_chars <= MAX_CHARS_PER_TEXT_RUN {
let run_id = parent_id.with(row_index).with(0usize);
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.
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;
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);
}
builder.set_character_positions(character_positions);
builder.set_character_widths(character_widths);
if row.ends_with_newline {
value.push('\n');
character_lengths.push(1);
character_positions.push(row.size.x);
character_widths.push(0.0);
}
word_lengths.push((character_lengths.len() - last_word_start) as _);
let chunk_word_starts: Vec<u8> = word_starts.iter().map(|&ws| ws as u8).collect();
builder.set_word_starts(chunk_word_starts);
});
} else {
let num_chunks = total_chars.div_ceil(MAX_CHARS_PER_TEXT_RUN);
let mut byte_offset = 0usize;
builder.set_value(value);
builder.set_character_lengths(character_lengths);
builder.set_character_positions(character_positions);
builder.set_character_widths(character_widths);
builder.set_word_lengths(word_lengths);
});
for chunk_idx in 0..num_chunks {
let char_start = chunk_idx * MAX_CHARS_PER_TEXT_RUN;
let char_end = (char_start + MAX_CHARS_PER_TEXT_RUN).min(total_chars);
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 {
action: accesskit::Action::SetTextSelection,
target,
target_node,
target_tree,
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 =
ccursor_from_accesskit_text_position(_widget_id, galley, &selection.focus);
let secondary =
@@ -224,18 +227,31 @@ fn ccursor_from_accesskit_text_position(
galley: &Galley,
position: &accesskit::TextPosition,
) -> Option<CCursor> {
use super::accesskit_text::MAX_CHARS_PER_TEXT_RUN;
let mut total_length = 0usize;
for (i, row) in galley.rows.iter().enumerate() {
let row_id = id.with(i);
if row_id.accesskit_id() == position.node {
return Some(CCursor {
index: total_length + position.character_index,
prefer_next_row: !(position.character_index == row.glyphs.len()
&& !row.ends_with_newline
&& (i + 1) < galley.rows.len()),
});
let row_chars = row.glyphs.len() + (row.ends_with_newline as usize);
let num_chunks = if row_chars == 0 {
1
} else {
row_chars.div_ceil(MAX_CHARS_PER_TEXT_RUN)
};
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
}

View File

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

View File

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

View File

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

View File

@@ -259,6 +259,13 @@ impl<'a> Button<'a> {
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.
pub fn atom_ui(self, ui: &mut Ui) -> AtomLayoutResponse {
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::{
Button, CursorIcon, Id, Key, MINUS_CHAR_STR, Modifiers, NumExt as _, Response, RichText, Sense,
TextEdit, TextWrapMode, Ui, Widget, WidgetInfo, emath, text,
Atom, AtomExt as _, AtomKind, Atoms, Button, CursorIcon, Id, IntoAtoms, Key, MINUS_CHAR_STR,
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> {
get_set_value: GetSetValue<'a>,
speed: f64,
prefix: String,
suffix: String,
atoms: Atoms<'a>,
range: RangeInclusive<f64>,
clamp_existing_to_range: bool,
min_decimals: usize,
@@ -50,6 +48,8 @@ pub struct DragValue<'a> {
}
impl<'a> DragValue<'a> {
const ATOM_ID: &'static str = "drag_item";
pub fn new<Num: emath::Numeric>(value: &'a mut Num) -> Self {
let slf = Self::from_get_set(move |v: Option<f64>| {
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 {
let atoms = Atoms::new(Atom::custom(Id::new(Self::ATOM_ID), Vec2::ZERO).atom_grow(true));
Self {
get_set_value: Box::new(get_set_value),
speed: 1.0,
prefix: Default::default(),
suffix: Default::default(),
atoms,
range: f64::NEG_INFINITY..=f64::INFINITY,
clamp_existing_to_range: true,
min_decimals: 0,
@@ -164,15 +165,15 @@ impl<'a> DragValue<'a> {
/// Show a prefix before the number, e.g. "x: "
#[inline]
pub fn prefix(mut self, prefix: impl ToString) -> Self {
self.prefix = prefix.to_string();
pub fn prefix(mut self, prefix: impl IntoAtoms<'a>) -> Self {
self.atoms.extend_left(prefix.into_atoms());
self
}
/// Add a suffix to the number, this can be e.g. a unit ("°" or " m")
#[inline]
pub fn suffix(mut self, suffix: impl ToString) -> Self {
self.suffix = suffix.to_string();
pub fn suffix(mut self, suffix: impl IntoAtoms<'a>) -> Self {
self.atoms.extend_right(suffix.into_atoms());
self
}
@@ -433,8 +434,7 @@ impl Widget for DragValue<'_> {
speed,
range,
clamp_existing_to_range,
prefix,
suffix,
mut atoms,
min_decimals,
max_decimals,
custom_formatter,
@@ -442,6 +442,23 @@ impl Widget for DragValue<'_> {
update_while_editing,
} = 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());
// The widget has the same ID whether it's in edit or button mode.
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 value_text = ui
.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));
response
} else {
let button = Button::new(
RichText::new(format!("{}{}{}", prefix, value_text.clone(), suffix))
.text_style(text_style),
)
.wrap_mode(TextWrapMode::Extend)
.sense(Sense::click_and_drag())
.min_size(ui.spacing().interact_size); // TODO(emilk): find some more generic solution to `min_size`
atoms.map_atoms(|atom| {
if atom.id == Some(atom_id) {
RichText::new(value_text.clone())
.text_style(text_style.clone())
.into()
} else {
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() {
CursorIcon::ResizeEast
@@ -607,10 +629,8 @@ impl Widget for DragValue<'_> {
if ui.style().explanation_tooltips {
response = response.on_hover_text(format!(
"{}{}{}\nDrag to edit or click to enter a value.\nPress 'Shift' while dragging for better control.",
prefix,
"{}\nDrag to edit or click to enter a value.\nPress 'Shift' while dragging for better control.",
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
// when in edit mode.
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);
}
});

View File

@@ -1,7 +1,7 @@
use std::mem;
use accesskit::{Action, ActionRequest, NodeId};
use accesskit_consumer::{FilterResult, Node, Tree, TreeChangeHandler};
use accesskit::{Action, ActionRequest};
use accesskit_consumer::{FilterResult, Node, NodeId, Tree, TreeChangeHandler};
use eframe::epaint::text::TextWrapMode;
use egui::{
@@ -25,7 +25,7 @@ use egui::{
pub struct AccessibilityInspectorPlugin {
pub open: bool,
tree: Option<accesskit_consumer::Tree>,
selected_node: Option<Id>,
selected_node: Option<NodeId>,
queued_action: Option<ActionRequest>,
}
@@ -113,13 +113,17 @@ impl AccessibilityInspectorPlugin {
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();
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 {
ui.debug_painter().debug_rect(
@@ -174,8 +178,10 @@ impl AccessibilityInspectorPlugin {
if node.supports_action(action, &|_node| FilterResult::Include)
&& ui.button(format!("{action:?}")).clicked()
{
let (target_node, target_tree) = node.locate();
let action_request = ActionRequest {
target: node.id(),
target_node,
target_tree,
action,
data: None,
};
@@ -188,8 +194,8 @@ impl AccessibilityInspectorPlugin {
}
}
fn node_ui(ui: &mut Ui, node: &Node<'_>, selected_node: &mut Option<Id>) {
if node.id() == Self::id().value().into()
fn node_ui(ui: &mut Ui, node: &Node<'_>, selected_node: &mut Option<NodeId>) {
if node.locate() == (Self::id().value().into(), accesskit::TreeId::ROOT)
|| node
.value()
.as_deref()
@@ -200,12 +206,12 @@ impl AccessibilityInspectorPlugin {
let label = node
.label()
.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);
// 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.id().0) };
let egui_node_id = unsafe { Id::from_high_entropy_bits(node.locate().0.0) };
ui.push_id(node.id(), |ui| {
let child_count = node.children().len();
@@ -228,7 +234,7 @@ impl AccessibilityInspectorPlugin {
collapsing.set_open(!collapsing.is_open());
}
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() {
let widget_response = ui.ctx().read_response(egui_node_id);

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:509020d8885b718900e534c9948cb95ae88e1eee9e113bdfb77a2f75b9a68f7b
size 96703
oid sha256:c8b23a5286e5d2dbd8d3eddac6583d981152bd791f74edfa5c712a610f795256
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)"),
);
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
{
use egui::TextBuffer as _;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b7bbd16c8aad444f0d11aacf87cf2292d494cc80a1ca46e7e8db86ca3041d35a
size 35931
oid sha256:ac6f9adeef92be9f69cb288ccafda8d522b8c3cde64352cd5369ae63668240c0
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
oid sha256:bbdc4199dee2ae853b8a240cd84528482dc6762233bd0d1249f2daa296b49487
size 64172
oid sha256:c5a45307147f19f2d69a3de1f53e0a73ba4c3368eb25a66b4098fb54cb83822f
size 64203

View File

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

View File

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

View File

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

View File

@@ -38,7 +38,7 @@ egui.workspace = true
eframe = { workspace = true, optional = true }
kittest.workspace = true
serde.workspace = true
toml.workspace = true
toml = {workspace = true, features = ["parse", "serde"] }
# wgpu dependencies
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 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.
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.
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.
/// In contrast to `click()`, this can also click widgets that are not currently visible.
pub fn click_accesskit(&self) {
let (target_node, target_tree) = self.accesskit_node.locate();
self.event(egui::Event::AccessKitActionRequest(
accesskit::ActionRequest {
target: self.accesskit_node.id(),
target_node,
target_tree,
action: accesskit::Action::Click,
data: None,
},
@@ -119,9 +121,11 @@ impl Node<'_> {
}
pub fn focus(&self) {
let (target_node, target_tree) = self.accesskit_node.locate();
self.event(egui::Event::AccessKitActionRequest(ActionRequest {
action: accesskit::Action::Focus,
target: self.accesskit_node.id(),
target_node,
target_tree,
data: None,
}));
}
@@ -162,45 +166,55 @@ impl Node<'_> {
/// Scroll the node into view.
pub fn scroll_to_me(&self) {
let (target_node, target_tree) = self.accesskit_node.locate();
self.event(egui::Event::AccessKitActionRequest(ActionRequest {
action: accesskit::Action::ScrollIntoView,
target: self.accesskit_node.id(),
target_node,
target_tree,
data: None,
}));
}
/// Scroll the [`egui::ScrollArea`] containing this node down (100px).
pub fn scroll_down(&self) {
let (target_node, target_tree) = self.accesskit_node.locate();
self.event(egui::Event::AccessKitActionRequest(ActionRequest {
action: accesskit::Action::ScrollDown,
target: self.accesskit_node.id(),
target_node,
target_tree,
data: None,
}));
}
/// Scroll the [`egui::ScrollArea`] containing this node up (100px).
pub fn scroll_up(&self) {
let (target_node, target_tree) = self.accesskit_node.locate();
self.event(egui::Event::AccessKitActionRequest(ActionRequest {
action: accesskit::Action::ScrollUp,
target: self.accesskit_node.id(),
target_node,
target_tree,
data: None,
}));
}
/// Scroll the [`egui::ScrollArea`] containing this node left (100px).
pub fn scroll_left(&self) {
let (target_node, target_tree) = self.accesskit_node.locate();
self.event(egui::Event::AccessKitActionRequest(ActionRequest {
action: accesskit::Action::ScrollLeft,
target: self.accesskit_node.id(),
target_node,
target_tree,
data: None,
}));
}
/// Scroll the [`egui::ScrollArea`] containing this node right (100px).
pub fn scroll_right(&self) {
let (target_node, target_tree) = self.accesskit_node.locate();
self.event(egui::Event::AccessKitActionRequest(ActionRequest {
action: accesskit::Action::ScrollRight,
target: self.accesskit_node.id(),
target_node,
target_tree,
data: None,
}));
}

View File

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

View File

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

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