1
0
mirror of https://github.com/emilk/egui.git synced 2026-06-27 07:03:14 -04:00

Merge branch 'emilk:main' into main

This commit is contained in:
AdrienZ.
2026-03-17 16:18:40 +01:00
committed by GitHub
175 changed files with 1981 additions and 1216 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,34 +120,36 @@ 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 }
web-sys = "0.3.77"
web-time = "1.1.0" # Timekeeping for native and web
webbrowser = "1.0.5"
wgpu = { version = "27.0.1", default-features = false, features = ["std"] }
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

@@ -268,6 +268,7 @@ impl WebPainter for WebPainterWgpu {
label: Some("egui_render"),
occlusion_query_set: None,
timestamp_writes: None,
multiview_mask: None,
});
// Forgetting the pass' lifetime means that we are no longer compile-time protected from
@@ -280,15 +281,13 @@ impl WebPainter for WebPainterWgpu {
);
}
let mut capture_buffer = None;
if capture && let Some(capture_state) = &mut self.screen_capture_state {
capture_buffer = Some(capture_state.copy_textures(
&render_state.device,
&output_frame,
&mut encoder,
));
}
let capture_buffer = if capture
&& let Some(capture_state) = &mut self.screen_capture_state
{
Some(capture_state.copy_textures(&render_state.device, &output_frame, &mut encoder))
} else {
None
};
Some((output_frame, capture_buffer))
};

View File

@@ -47,7 +47,7 @@ impl CaptureState {
},
depth_stencil: None,
multisample: MultisampleState::default(),
multiview: None,
multiview_mask: None,
cache: None,
});
@@ -165,6 +165,7 @@ impl CaptureState {
depth_stencil_attachment: None,
occlusion_query_set: None,
timestamp_writes: None,
multiview_mask: None,
});
pass.set_pipeline(&self.pipeline);

View File

@@ -185,7 +185,7 @@ impl RenderState {
wgpu::Backends::all()
};
instance.enumerate_adapters(backends)
instance.enumerate_adapters(backends).await
};
let (adapter, device, queue) = match config.wgpu_setup.clone() {
@@ -395,6 +395,10 @@ pub fn adapter_info_summary(info: &wgpu::AdapterInfo) -> String {
driver,
driver_info,
backend,
device_pci_bus_id,
subgroup_min_size,
subgroup_max_size,
transient_saves_memory,
} = &info;
// Example values:
@@ -426,6 +430,13 @@ pub fn adapter_info_summary(info: &wgpu::AdapterInfo) -> String {
if *device != 0 {
summary += &format!(", device: 0x{device:02X}");
}
if !device_pci_bus_id.is_empty() {
summary += &format!(", pci_bus_id: {device_pci_bus_id:?}");
}
if *subgroup_min_size != 0 || *subgroup_max_size != 0 {
summary += &format!(", subgroup_size: {subgroup_min_size}..={subgroup_max_size}");
}
summary += &format!(", transient_saves_memory: {transient_saves_memory}");
summary
}

View File

@@ -353,7 +353,7 @@ impl Renderer {
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("egui_pipeline_layout"),
bind_group_layouts: &[&uniform_bind_group_layout, &texture_bind_group_layout],
push_constant_ranges: &[],
immediate_size: 0,
});
let depth_stencil = options
@@ -426,7 +426,7 @@ impl Renderer {
})],
compilation_options: wgpu::PipelineCompilationOptions::default()
}),
multiview: None,
multiview_mask: None,
cache: None,
}
)

View File

@@ -362,14 +362,13 @@ impl Painter {
#[cfg(all(target_os = "macos", feature = "macos-window-resize-jitter-fix"))]
{
// SAFETY: The cast is checked with if condition. If the used backend is not metal
// it gracefully fails. The pointer casts are valid as it's 1-to-1 type mapping.
// This is how wgpu currently exposes this backend-specific flag.
// it gracefully fails.
unsafe {
if let Some(hal_surface) = state.surface.as_hal::<wgpu::hal::api::Metal>() {
let raw =
std::ptr::from_ref::<wgpu::hal::metal::Surface>(&*hal_surface).cast_mut();
(*raw).present_with_transaction = resizing;
hal_surface
.render_layer()
.lock()
.set_presents_with_transaction(resizing);
Self::configure_surface(
state,
@@ -421,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;
};
@@ -554,6 +581,7 @@ impl Painter {
}),
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
// Forgetting the pass' lifetime means that we are no longer compile-time protected from
@@ -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>,
@@ -548,23 +548,23 @@ impl State {
///
/// | Setup | Events in Order |
/// | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
/// | a-macos15-apple_shuangpin | `Predict("", None)` -> `Commit("测试")` |
/// | b-debian13_gnome48_wayland-fcitx5_shuangpin | `Predict("", None)` -> `Commit("测试")` -> `Predict("", Some(0, 0))` -> `Predict("", None)` (duplicate until `TextEdit` blurred) |
/// | c-windows11-ms_pinyin | `Predict("测试", Some(…))` -> `Predict("", None)` -> `Commit("测试")` -> `Disabled` |
/// | a-macos15-apple_shuangpin | `Preedit("", None)` -> `Commit("测试")` |
/// | b-debian13_gnome48_wayland-fcitx5_shuangpin | `Preedit("", None)` -> `Commit("测试")` -> `Preedit("", Some(0, 0))` -> `Preedit("", None)` (duplicate until `TextEdit` blurred) |
/// | c-windows11-ms_pinyin | `Preedit("测试", Some(…))` -> `Preedit("", None)` -> `Commit("测试")` -> `Disabled` |
///
/// #### Situation: pressed backspace to delete the last character in the prediction
/// #### Situation: pressed backspace to delete the last character in the composition
///
/// | Setup | Events in Order |
/// | a-macos15-apple_shuangpin | `Predict("", None)` |
/// | b-debian13_gnome48_wayland-fcitx5_shuangpin | `Predict("", Some(0, 0))` -> `Predict("", None)` (duplicate until `TextEdit` blurred) |
/// | c-windows11-ms_pinyin | `Predict("", Some(0, 0))` -> `Predict("", None)` -> `Commit("")` -> `Disabled` |
/// | a-macos15-apple_shuangpin | `Preedit("", None)` |
/// | b-debian13_gnome48_wayland-fcitx5_shuangpin | `Preedit("", Some(0, 0))` -> `Preedit("", None)` (duplicate until `TextEdit` blurred) |
/// | c-windows11-ms_pinyin | `Preedit("", Some(0, 0))` -> `Preedit("", None)` -> `Commit("")` -> `Disabled` |
///
/// #### Situation: clicked somewhere else while there is an active composition with the prediction "ce"
/// #### Situation: clicked somewhere else while there is an active composition with the pre-edit text "ce"
///
/// | Setup | Events in Order |
/// | ------------------------------------------- | ------------------------------------------------------------------------------------------------- |
/// | a-macos15-apple_shuangpin | nothing emitted |
/// | b-debian13_gnome48_wayland-fcitx5_shuangpin | `Predict("", Some(0, 0))` (duplicate) -> `Predict("", None)` (duplicate until `TextEdit` blurred) |
/// | b-debian13_gnome48_wayland-fcitx5_shuangpin | `Preedit("", Some(0, 0))` (duplicate) -> `Preedit("", None)` (duplicate until `TextEdit` blurred) |
/// | c-windows11-ms_pinyin | nothing emitted |
fn on_ime(&mut self, ime: &winit::event::Ime) {
// // code for inspecting ime events emitted by winit:
@@ -610,15 +610,26 @@ impl State {
self.ime_event_disable();
}
winit::event::Ime::Preedit(_, None) => {
// we need to emit this on macOS, since winit doesn't emit
// `Predict("", Some(0, 0))` before this event on macOS when the
// user deletes the last character in the prediction with the
// backspace key. Without this, only `egui::ImeEvent::Disabled`
// is emitted here, leading to the last character being left in
// TextEdit in such situation.
self.egui_input
.events
.push(egui::Event::Ime(egui::ImeEvent::Preedit(String::new())));
if cfg!(target_os = "macos") {
// On macOS, when the user presses backspace to delete the
// last character in an IME composition, `winit` only emits
// `winit::event::Ime::Preedit("", None)` without a
// preceding `winit::event::Ime::Preedit("", Some(0, 0))`.
//
// The current implementation of `egui::TextEdit` relies on
// receiving an `egui::ImeEvent::Preedit("")` to remove the
// last character in the composition in this case, so we
// emit it here.
//
// This is guarded to macOS-only, as applying it on other
// platforms is unnecessary and can cause undesired
// behavior.
// See: https://github.com/emilk/egui/pull/7973
self.egui_input
.events
.push(egui::Event::Ime(egui::ImeEvent::Preedit(String::new())));
}
self.ime_event_disable();
}
}
@@ -640,11 +651,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

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

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

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

@@ -1066,26 +1066,36 @@ fn events(
} => check_for_mutating_key_press(os, &cursor_range, text, galley, modifiers, *key),
Event::Ime(ime_event) => {
/// Empty prediction can be produced with [`ImeEvent::Preedit`]
/// or [`ImeEvent::Commit`] when user press backspace or escape
/// during IME, so this function should be called in both cases
/// to clear current text.
/// Both `ImeEvent::Preedit("")` and `ImeEvent::Commit("")`
/// might be emitted from different integrations to signify that
/// the current IME composition should be cleared.
///
/// Example platforms where only `ImeEvent::Preedit("")` of
/// those two events is emitted when the last character in the
/// prediction is deleted:
/// - macOS 15.7.3.
/// - Debian13 with gnome48 and wayland.
/// Example integrations where only `ImeEvent::Preedit("")` of
/// those two events is emitted when the last character is
/// deleted with a backspace:
/// - `egui-winit` on macOS 15.7.3.
/// - `egui-winit` on Debian13 with gnome48 and wayland.
///
/// An example platform where only `ImeEvent::Commit("")` of
/// those two events is emitted when the last character in the
/// prediction is deleted:
/// - Safari 26.2 (on macOS 15.7.3).
fn clear_prediction(
/// An example integration where only `ImeEvent::Commit("")` of
/// those two events is emitted when the last character is
/// deleted with a backspace:
/// - `eframe`'s web integration on Safari 26.2 (on macOS
/// 15.7.3).
///
/// ## Note
///
/// The term “pre-edit string” is used by X11 and Wayland, and
/// we use “pre-edit text” and “pre-edit range” here in the
/// same manner.
/// See: <https://wayland.app/protocols/input-method-unstable-v2>
///
/// We previously referred to “pre-edit text” as “prediction”,
/// which is not standard and can mean different things.
fn clear_preedit_text(
text: &mut dyn TextBuffer,
cursor_range: &CCursorRange,
preedit_range: &CCursorRange,
) -> CCursor {
text.delete_selected(cursor_range)
text.delete_selected(preedit_range)
}
match ime_event {
@@ -1094,33 +1104,33 @@ fn events(
state.ime_cursor_range = cursor_range;
None
}
ImeEvent::Preedit(text_mark) => {
if text_mark == "\n" || text_mark == "\r" {
ImeEvent::Preedit(preedit_text) => {
if preedit_text == "\n" || preedit_text == "\r" {
None
} else {
let mut ccursor = clear_prediction(text, &cursor_range);
let mut ccursor = clear_preedit_text(text, &cursor_range);
let start_cursor = ccursor;
if !text_mark.is_empty() {
text.insert_text_at(&mut ccursor, text_mark, char_limit);
if !preedit_text.is_empty() {
text.insert_text_at(&mut ccursor, preedit_text, char_limit);
}
state.ime_cursor_range = cursor_range;
Some(CCursorRange::two(start_cursor, ccursor))
}
}
ImeEvent::Commit(prediction) => {
if prediction == "\n" || prediction == "\r" {
ImeEvent::Commit(commit_text) => {
if commit_text == "\n" || commit_text == "\r" {
None
} else {
state.ime_enabled = false;
let mut ccursor = clear_prediction(text, &cursor_range);
let mut ccursor = clear_preedit_text(text, &cursor_range);
if !prediction.is_empty()
if !commit_text.is_empty()
&& cursor_range.secondary.index
== state.ime_cursor_range.secondary.index
{
text.insert_text_at(&mut ccursor, prediction, char_limit);
text.insert_text_at(&mut ccursor, commit_text, char_limit);
}
Some(CCursorRange::one(ccursor))

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

@@ -41,7 +41,7 @@ impl Custom3d {
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("custom3d"),
bind_group_layouts: &[&bind_group_layout],
push_constant_ranges: &[],
immediate_size: 0,
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
@@ -62,7 +62,7 @@ impl Custom3d {
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview: None,
multiview_mask: None,
cache: None,
});

View File

@@ -219,6 +219,10 @@ fn integration_ui(ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
driver,
driver_info,
backend,
device_pci_bus_id,
subgroup_min_size,
subgroup_max_size,
transient_saves_memory,
} = &info;
// Example values:
@@ -261,6 +265,19 @@ fn integration_ui(ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
ui.label(format!("0x{device:02X}"));
ui.end_row();
}
if !device_pci_bus_id.is_empty() {
ui.label("PCI Bus ID:");
ui.label(device_pci_bus_id.as_str());
ui.end_row();
}
if *subgroup_min_size != 0 || *subgroup_max_size != 0 {
ui.label("Subgroup size:");
ui.label(format!("{subgroup_min_size}..={subgroup_max_size}"));
ui.end_row();
}
ui.label("Transient saves memory:");
ui.label(format!("{transient_saves_memory}"));
ui.end_row();
});
};

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 }

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