mirror of
https://github.com/emilk/egui.git
synced 2026-06-26 22:53:14 -04:00
Merge branch 'main' into style_modifier
This commit is contained in:
@@ -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"
|
||||
|
||||
809
Cargo.lock
809
Cargo.lock
File diff suppressed because it is too large
Load Diff
23
Cargo.toml
23
Cargo.toml
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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`].
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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`].
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1066,7 +1066,7 @@ impl CentralPanel {
|
||||
id,
|
||||
UiBuilder::new()
|
||||
.layer_id(LayerId::background())
|
||||
.max_rect(ctx.available_rect().round_ui()),
|
||||
.max_rect(ctx.available_rect()),
|
||||
);
|
||||
panel_ui.set_clip_rect(ctx.content_rect());
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -798,7 +798,7 @@ impl Context {
|
||||
Id::new((ctx.viewport_id(), "__top_ui")),
|
||||
UiBuilder::new()
|
||||
.layer_id(LayerId::background())
|
||||
.max_rect(ctx.available_rect().round_ui()),
|
||||
.max_rect(ctx.available_rect()),
|
||||
);
|
||||
|
||||
{
|
||||
@@ -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;
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -543,22 +543,19 @@ impl Focus {
|
||||
..
|
||||
} = event
|
||||
&& let Some(cardinality) = match key {
|
||||
crate::Key::ArrowUp => Some(FocusDirection::Up),
|
||||
crate::Key::ArrowRight => Some(FocusDirection::Right),
|
||||
crate::Key::ArrowDown => Some(FocusDirection::Down),
|
||||
crate::Key::ArrowLeft => Some(FocusDirection::Left),
|
||||
crate::Key::ArrowUp if !modifiers.any() => Some(FocusDirection::Up),
|
||||
crate::Key::ArrowRight if !modifiers.any() => Some(FocusDirection::Right),
|
||||
crate::Key::ArrowDown if !modifiers.any() => Some(FocusDirection::Down),
|
||||
crate::Key::ArrowLeft if !modifiers.any() => Some(FocusDirection::Left),
|
||||
|
||||
crate::Key::Tab => {
|
||||
if modifiers.shift {
|
||||
Some(FocusDirection::Previous)
|
||||
} else {
|
||||
Some(FocusDirection::Next)
|
||||
}
|
||||
}
|
||||
crate::Key::Escape => {
|
||||
crate::Key::Tab if !modifiers.any() => Some(FocusDirection::Next),
|
||||
crate::Key::Tab if modifiers.shift_only() => Some(FocusDirection::Previous),
|
||||
|
||||
crate::Key::Escape if !modifiers.any() => {
|
||||
self.focused_widget = None;
|
||||
Some(FocusDirection::None)
|
||||
}
|
||||
|
||||
_ => None,
|
||||
}
|
||||
{
|
||||
@@ -567,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::{Layout, Painter, Pos2, Rect, Region, Vec2, grid, vec2};
|
||||
use emath::GuiRounding as _;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
use crate::{Align2, Color32, Stroke};
|
||||
@@ -92,6 +93,7 @@ impl Placer {
|
||||
} else {
|
||||
self.layout.available_rect_before_wrap(&self.region)
|
||||
}
|
||||
.round_ui()
|
||||
}
|
||||
|
||||
/// Amount of space available for a widget.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -55,8 +55,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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -261,6 +261,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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:72c403249fb7e91f225992044153ce500d3971f595fb8ec3a8fb78ecc0e998c7
|
||||
size 335365
|
||||
oid sha256:80c64fe75dfc412bb33aa977055afcb140e6bfff19128ecf5bd35318ab6773d0
|
||||
size 335380
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:706ad012e52a8c51175b050b985cca88e2cb306b24f618b7391641397d17cd28
|
||||
size 92804
|
||||
oid sha256:cc55083688d043234c37d22c74635a44b00f4d28c3802c4327c2eaf563c73eed
|
||||
size 92800
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4135662f2b60a10ef8c3b155172d7a3edcf24a625d8286aeaad0614aa8819893
|
||||
size 169604
|
||||
oid sha256:78d91fa4657cd1cb375487f606b80d418ed6fdbd8a0c0225b9383eead5001563
|
||||
size 169682
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:509020d8885b718900e534c9948cb95ae88e1eee9e113bdfb77a2f75b9a68f7b
|
||||
size 96703
|
||||
oid sha256:c8b23a5286e5d2dbd8d3eddac6583d981152bd791f74edfa5c712a610f795256
|
||||
size 96759
|
||||
|
||||
@@ -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 _;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:52d2233594c4bad348f5479dcfad9576ee5fd7d49faedb6f5ba74b374cdaf3ad
|
||||
oid sha256:a53262cf5d8507d8eeae8c968767cef462b727879245085673982b850a6da670
|
||||
size 26977
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:47f6cd15b88df83a9b2d8538e424041e661712f2e85312166a581f69f1254643
|
||||
size 26839
|
||||
oid sha256:75a9cd9a3315b236c23a53e890de1a821d39c3327813d06df85ba86d2ed50cc7
|
||||
size 26887
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9302478abb0b86fae1af3af45d91f032272a56a2098405525d08aba4f9534644
|
||||
size 76103
|
||||
oid sha256:a7601584308bf60820506f842569a3c1daf3c15fa6e715f6b9386b5112dcc92f
|
||||
size 76076
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6927950ccbc5c81d6fbfe0a90ddd79a4306518caced14bb60debd30c7e41d326
|
||||
oid sha256:d4e33c7f817100d8414bba245ee7886354b86109f383d59e87a197e39501f0a0
|
||||
size 62604
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:366d18457aabf1ebdd42fdbce8819cc67a4f59db85c452623b02ee1d0e8fc50a
|
||||
size 27817
|
||||
oid sha256:93fcc271831167cb077f3de0a9f0e27037f9e5a2ce94e056bd6f1ede9890cb7e
|
||||
size 27818
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3c5e803659e936268b476690427ef6a6802f477e078dc956a9d1c857b48da868
|
||||
size 114409
|
||||
oid sha256:3fc2793506ec483c7f124b6206fb18ffb73bec29746f2d9bb5145042ddc45016
|
||||
size 114410
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f5e67baf0696792e50f7ab3121874d055ddee2de0514712aacbf8e135ec4743d
|
||||
size 25425
|
||||
oid sha256:20ea4f93ee50c7a3585aef74c66d7700083ac1c16519b0704b70387849d9d2bc
|
||||
size 25057
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:55c5fb90736a31fbccd72be5994fc8c62b4b9da9842ad1e6bb795a1e1461a6f8
|
||||
size 98780
|
||||
oid sha256:1b72a4c0e6d441190a7a156b8bba709e81b6c1fe7b0eacedc1ee7a3bfcf881f6
|
||||
size 99297
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a4080ee1a16eea16c8f4246fe3e760ade7d0289b30d88068d1e49ffb88d88dca
|
||||
size 18280
|
||||
oid sha256:08c40934d4bd2a239bdcc1928d1e5eba56bac03fdded2c85cf47b020d669f07e
|
||||
size 18281
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:600d9e0fc193396f36b599e4bfad2547128160d2e56dc2a989cb5f978d5115ae
|
||||
size 113797
|
||||
oid sha256:82878e4150e38fdc4b2e78203c8c661c2d9e716ab32595c298392faf6ba96105
|
||||
size 113803
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3ae5e843cc9d847b0f3c4092f55b914699adb506cb807b0a97bfc4ec7d94537b
|
||||
size 22613
|
||||
oid sha256:58cd3aba4392332a45f57c7dd90a9b5da386cb396c0c6319e7a7dae71e03ff30
|
||||
size 22563
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b0d38cb1eebf3ce7d661d094175b425db2b9eccc5e439b14256c5d801d4454d4
|
||||
size 47285
|
||||
oid sha256:26ffcf6b71108b82ce15d4cf3f9dd0ce9fe0b9563f02725fef1b74f40e749439
|
||||
size 47281
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:00e4c7659cd50044d473dd2c138392f78ac7eba27f2b52bae61246f5dc5b2782
|
||||
size 23156
|
||||
oid sha256:faedf9631149e231d510165215c24fccec50502d58000d5f893aa047a637a68f
|
||||
size 23148
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:604f716e687fc26abba92769fe2dae75d850b18598d2e8a9524451ab0f760251
|
||||
size 65403
|
||||
oid sha256:b6b4c2e55c02fa4caf5f9f8bd2d8c0311cc4cbcf1fc2f568fe112e8e6125c675
|
||||
size 65308
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:34de6fd788288174e8e6f1fa48cd49dbc7b14fcf649fe302aed49c8c50178aa8
|
||||
oid sha256:3a65927cd8bd8d24e3ffbea8eb421eb22849b27dc77d36f8acd82bf5d5e63959
|
||||
size 33469
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:96760220222bdde8dd1b3d28f089af2892403b78df8d34d3d94dc1a604387083
|
||||
size 18241
|
||||
oid sha256:9c595ee9b7ada33780178a6a35e26a98055a707f2ff99f6bb36e8db4ed819791
|
||||
size 18242
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:48d138634343edb251435bf6f9075502b913e806e8b280f3e6012977c13af16f
|
||||
size 56753
|
||||
oid sha256:c218115d305dfa6c9ab883ac6f3a21584b4840b3ba273ea765c8a8381d78935f
|
||||
size 57181
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:23efb79ca13367f4d8886142d015815c5bdf99c0ed243ece294a7cfd365fd166
|
||||
size 33503
|
||||
oid sha256:4d10b78f4d80d61a3352d7f2b0ed9b2d93af5f184f2487f6f2afff02a38f4608
|
||||
size 33475
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5d85faf6e7fa26741eb720e74695f3c207ea15097b118c3cafe5d52d5d85ea20
|
||||
size 23666
|
||||
oid sha256:f2ce9062c5d1f0b0861d5df49ae64e56ba0e6501e8bd3f8a92c53aea748be78b
|
||||
size 23629
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:875eb687f3a1eed52a6617e532edc5332b0a16296e2b6addac66d5bea0448b14
|
||||
size 172605
|
||||
oid sha256:b5b965a7c690fd8e8646812513e2417170b687fd37e29d220c29127ba0cc200c
|
||||
size 172609
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fd3573be9ba5818b4edc371095f5c23b084e6c7eaae4f2fd3a6d2de051878c9d
|
||||
size 118567
|
||||
oid sha256:6ffba8bb50b42e47f855f62682f6d5ec10bf67b01d3aa2e843f6bf787f150d0d
|
||||
size 118562
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ed1be0294fb65b11c54c6dc9e4cecb383ace16dad748e3c42f2ed65b2fb05ea8
|
||||
size 75509
|
||||
oid sha256:931f38ade8373ff79801c05c5d4397f2c5fcfa27022f2e1abe9eb29d561a3aef
|
||||
size 76022
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7336c53885add09360df098b6b131323e8ad3ef0ec2b85bf022e78bc4269276a
|
||||
size 70255
|
||||
oid sha256:57bf5220ae8f47485a07e9117abaaad36924d8c6c0f9e278cb05c455f342bff6
|
||||
size 70250
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:411dd61eb182a70d46c7fc1fa0f9a4b8aeae88d08b11d5af948c5acccfa9d133
|
||||
size 60950
|
||||
oid sha256:7c964d07a39ad286a562b53cdfe514d568d91955e6c1ca06a0cb5e45dbe3977e
|
||||
size 60947
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:78111e33d44a09beb9c1233dd2d5ef10103213a1c1c7df8b5e258d9684f1d93a
|
||||
size 21810
|
||||
oid sha256:718203d31d8b027a7718a66c4712cf1e17b9aea2e870d755bd2c0c346529d4f4
|
||||
size 21814
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e9498a706de403ee7db3603ecc896688e584fede367ed6087cdf10b798a3ab2d
|
||||
oid sha256:6af5adc42544171c6d85e190c853aca06784c131a373a693a6f7069d4cf1a404
|
||||
size 13698
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:42385da2eb74d54ba086aed973ade15f2a8d2be0c9281c05e6fb88846137bf81
|
||||
size 35870
|
||||
oid sha256:2e8e03c2a42e195e6489659053aecb78755d3c218558cb2e9339fa7b6db59405
|
||||
size 35875
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9f1b6fa0c48479606539f2d98befe1c9ee881846c0b55d7a53313962d556380d
|
||||
size 484629
|
||||
oid sha256:ad22ea6b6e69fd71416fdae76cbd142d279f8f562e74b77e63b3989be187c57c
|
||||
size 484631
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:89074b8dab103a419bc3dac743da4d8c47f435fa55b98d8aab71f6c9fb4d39de
|
||||
size 12370
|
||||
oid sha256:c8ea98c65376d9f6ac66d0a9471c4bf3add0904294e7ca1a105458b90654a2e2
|
||||
size 12476
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7bd7b54ff60859e4d4793000bef3adbec4c071063bec6bfdbde62516c4fc3478
|
||||
size 12959
|
||||
oid sha256:3793a5e83ef9bdffef99bcd8905a094acb69cde356e3a7125a544045296c3926
|
||||
size 13070
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b53b03212953e12915a0e41bff5f0cdea90f8f866220a01142edaeb915735a34
|
||||
size 47077
|
||||
oid sha256:941582e2e20a9459db1f2cb7f07fa1930acfdb12cbbe7f96f9aafbeabf8b37f6
|
||||
size 47076
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:22e5d61a141b5a8663feb8a47371f9259d2a77fdacb1245bce411ffc85ce2cae
|
||||
size 47716
|
||||
oid sha256:2735a021f171f5c95888cda76e8668e1e023588c8c6c7cd382c03d8e31988fe3
|
||||
size 48209
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:120558ab0c267650744bd078aeace8d4122b3569c5998602f969766131d15c44
|
||||
size 43894
|
||||
oid sha256:867bef6b55b73d127306a461e115b6f0047d582904999de80aeabae00e60c967
|
||||
size 44295
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:af92548b6c8569081a91cb772b73988d9cb342498ddf9c0c86b6963cef8eda9e
|
||||
size 43985
|
||||
oid sha256:936ec8b223ae7f0f32c640c127e1b6b14033bb7d168a4d1f0e6b3bd08a761e36
|
||||
size 44055
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:db510af76578693c85ce78ca91224758a56f7bbf33db3221c9a4edca08b06600
|
||||
size 590547
|
||||
oid sha256:fba7387f5deba5e144e2106154b15ab956a50a418857bd34e16b306d7f1a29e4
|
||||
size 588252
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cae2b789e8afff23b7545d42a530e6c972d28736bad2bdacbc69f0e7065f85cc
|
||||
size 740660
|
||||
oid sha256:4656f3255d7859c07b269ff655eafe21bdddb949a07aa91477b826f6e2af8c28
|
||||
size 740616
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:09d9f567ec371d60881b525ddb462d9135552db97af5921a6eb02aba40e40616
|
||||
size 971544
|
||||
oid sha256:b18ff644ba5bd0c7f094bf8eac079d8a72bc6918638b1b110002f2f0a7a362cc
|
||||
size 967860
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3c383dd89fda6094704027074a72085591339a276d60502626d78e8e527b2e10
|
||||
size 1076719
|
||||
oid sha256:134caff5b8a4969055c32e8f51ca9c6eae1528b84d348691d860913e839de0d9
|
||||
size 1076746
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0b4559541cf3259496c760a26f8d83e82179cb7e4576333682c5af49ee4a35a7
|
||||
size 1125331
|
||||
oid sha256:d731b4ce039315e096113f3c83168165020949e57564e641e778728e35901169
|
||||
size 1125286
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:67c8412a1e8fdbfd88f8573797fbf6fbd89c6ce783a074a8e90f7d8d9e67dd57
|
||||
size 1366351
|
||||
oid sha256:cfac3518220555984d47c9fdfea2202a37102250aefcc2509794f337b3a7baae
|
||||
size 1361407
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a2b7b54a1af0f5cd31bd64f0506e3035dd423314ce3389e61730fa160434fbf3
|
||||
size 45074
|
||||
oid sha256:cf21fe763e9762bca1b0f486e29a6024efcbc106a7f1ac195104acd0621cf8db
|
||||
size 45107
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7b66a0be67ff2d684a54c2321123521b3ad06dfe5ebffd50e89260d77efcfcc4
|
||||
size 86833
|
||||
oid sha256:2f09338e652b965cc9ae7bbb261845cd9c15d79f3d15f3c5b5326ef6d163b606
|
||||
size 86885
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:19320291c99a23429b114a59de4636689e281e1e68766abe2aa1e56562128e50
|
||||
size 118919
|
||||
oid sha256:e298244953653e46875053b12b4fe06ee692cb58fc131233ac4172677f0f8b44
|
||||
size 118961
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5edf089c00715f1456fe7838e85aadcfc42b6216a3fd95b48d9c21fc8d700cba
|
||||
size 51371
|
||||
oid sha256:6b9b36acf821cca71f97a3c8468fb925561f3bc2030742aef1e3c1d9e69ccc6f
|
||||
size 51419
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6cd1a10639dcb323bdc3b2c43e0c35665184fc809731ced90088ee9edb9de845
|
||||
size 54577
|
||||
oid sha256:f5ad7a37546d48fc5426c32534a1c452fd0bf8280346dbe6e67ac26f17f3ba8a
|
||||
size 54626
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:87e34024f701dc93f4026213ac7eb468a2cd6d3393eb0dbec382bf58007f8e61
|
||||
size 55042
|
||||
oid sha256:c0b61e9d1c2bcbf891a7acd4f3c1d2bd7524133d8165e7e7984998670de5a085
|
||||
size 55090
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d7940ff56796efb27bec66b632ff33aa2ad390c4962a711bf520aee341f035a4
|
||||
size 35968
|
||||
oid sha256:a2e4975e9328a6d72f2c932daddfbb00cebdb2249aceb53f667d4060a1c0ea8a
|
||||
size 36006
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b7bbd16c8aad444f0d11aacf87cf2292d494cc80a1ca46e7e8db86ca3041d35a
|
||||
size 35931
|
||||
oid sha256:ac6f9adeef92be9f69cb288ccafda8d522b8c3cde64352cd5369ae63668240c0
|
||||
size 35973
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0475c5ac04ab8f79b79d43cfdb985f05b61dbe90e81f898a6dc216c308a28841
|
||||
size 4707
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:344d90928510855dc718a2e36e31a97f084f1163ab750d0217fb8620469b621a
|
||||
size 5276
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:60449af267336663304e44e254d0984e037bebfa2d1efdf32234cab4374e8c79
|
||||
size 5301
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user