Merge branch 'main' into lucas/malmal/main
10
.github/workflows/enforce_branch_name.yml
vendored
@@ -4,17 +4,23 @@ on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened, synchronize]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
check-source-branch:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Check PR source branch
|
||||
env:
|
||||
IS_FORK: ${{ github.event.pull_request.head.repo.fork }}
|
||||
HEAD_REF: ${{ github.event.pull_request.head.ref }}
|
||||
run: |
|
||||
# Check if PR is from a fork
|
||||
if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then
|
||||
if [[ "$IS_FORK" == "true" ]]; then
|
||||
# Check if PR is from the master/main branch of a fork
|
||||
if [[ "${{ github.event.pull_request.head.ref }}" == "master" || "${{ github.event.pull_request.head.ref }}" == "main" ]]; then
|
||||
if [[ "$HEAD_REF" == "master" || "$HEAD_REF" == "main" ]]; then
|
||||
echo "ERROR: Pull requests from the master/main branch of forks are not allowed, because it prevents maintainers from contributing to your PR"
|
||||
echo "Please create a feature branch in your fork and submit the PR from that branch instead."
|
||||
exit 1
|
||||
|
||||
2
.github/workflows/rust.yml
vendored
@@ -94,7 +94,7 @@ jobs:
|
||||
- name: wasm-bindgen
|
||||
uses: jetli/wasm-bindgen-action@v0.1.0
|
||||
with:
|
||||
version: "0.2.100" # Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml
|
||||
version: "0.2.108" # Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml
|
||||
|
||||
- run: ./scripts/wasm_bindgen_check.sh --skip-setup
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
1268
Cargo.lock
46
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",
|
||||
@@ -80,33 +80,34 @@ arboard = { version = "3.6.1", default-features = false }
|
||||
backtrace = "0.3.76"
|
||||
bitflags = "2.9.4"
|
||||
bytemuck = "1.24.0"
|
||||
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 }
|
||||
ehttp = { version = "0.7.1", default-features = false }
|
||||
enum-map = "2.7.3"
|
||||
env_logger = { version = "0.11.8", default-features = false }
|
||||
glow = "0.16.0"
|
||||
font-types = { version = "0.11.0", default-features = false, features = ["std"] }
|
||||
glow = "0.17.0"
|
||||
glutin = { version = "0.32.3", default-features = false }
|
||||
glutin-winit = { version = "0.5.0", default-features = false }
|
||||
home = "0.5.9"
|
||||
image = { version = "0.25.6", default-features = false }
|
||||
jiff = { version = "0.2.23", default-features = false }
|
||||
js-sys = "0.3.77"
|
||||
kittest = { version = "0.3.0" }
|
||||
kittest = { version = "0.4.0" }
|
||||
log = { version = "0.4.28", features = ["std"] }
|
||||
memoffset = "0.9.1"
|
||||
mimalloc = "0.1.48"
|
||||
mime_guess2 = { version = "2.3.1", default-features = false }
|
||||
mint = "0.5.9"
|
||||
nohash-hasher = "0.2.0"
|
||||
objc2 = "0.5.2"
|
||||
objc2-app-kit = { version = "0.2.2", default-features = false }
|
||||
objc2-foundation = { version = "0.2.2", default-features = false }
|
||||
objc2-ui-kit = { version = "0.2.2", default-features = false }
|
||||
objc2 = "0.6.4"
|
||||
objc2-app-kit = { version = "0.3.2", default-features = false }
|
||||
objc2-foundation = { version = "0.3.2", default-features = false }
|
||||
objc2-ui-kit = { version = "0.3.2", default-features = false }
|
||||
open = "5.3.2"
|
||||
parking_lot = "0.12.5"
|
||||
percent-encoding = "2.3.2"
|
||||
@@ -119,34 +120,33 @@ 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"
|
||||
ron = "0.11.0"
|
||||
rfd = "0.17.2"
|
||||
ron = "0.12.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.0.0", 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"] }
|
||||
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"
|
||||
vello_cpu = { version = "0.0.6", default-features = false, features = ["std", "u8_pipeline", "f32_pipeline"] }
|
||||
wasm-bindgen = "0.2.108" # Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml. Don't update this spuriously, because of https://github.com/rerun-io/rerun/issues/8766
|
||||
wasm-bindgen-futures = "0.4.58"
|
||||
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 = "29.0.0", default-features = false, features = ["std"] }
|
||||
windows-sys = "0.61.2"
|
||||
winit = { version = "0.30.12", default-features = false }
|
||||
|
||||
winit = { version = "0.30.13", default-features = false }
|
||||
|
||||
[workspace.lints.rust]
|
||||
unsafe_code = "deny"
|
||||
|
||||
10
README.md
@@ -10,16 +10,14 @@
|
||||
[](https://discord.gg/JFcEma9bJq)
|
||||
|
||||
|
||||
<br/>
|
||||
<div align="center">
|
||||
<a href="https://www.rerun.io/"><img src="https://github.com/user-attachments/assets/78e79463-4357-461b-bbd1-31aa5ef5e1a2" width="250"></a>
|
||||
<a href="https://www.egui.rs/"><img src="https://github.com/user-attachments/assets/cfaf1d43-9338-490f-ae82-99b420baa1b0" width="400"></a>
|
||||
|
||||
egui development is sponsored by [Rerun](https://www.rerun.io/), a startup building<br>
|
||||
an SDK for visualizing streams of multimodal data.
|
||||
</div>
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
👉 [Click to run the web demo](https://www.egui.rs/#demo) 👈
|
||||
</div>
|
||||
|
||||
egui (pronounced "e-gooey") is a simple, fast, and highly portable immediate mode GUI library for Rust. egui runs on the web, natively, and [in your favorite game engine](#integrations).
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ readme = "README.md"
|
||||
repository = "https://github.com/emilk/egui"
|
||||
categories = ["mathematics", "encoding"]
|
||||
keywords = ["gui", "color", "conversion", "gamedev", "images"]
|
||||
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
|
||||
include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -11,7 +11,7 @@ readme = "README.md"
|
||||
repository = "https://github.com/emilk/egui/tree/main/crates/eframe"
|
||||
categories = ["gui", "game-development"]
|
||||
keywords = ["egui", "gui", "gamedev"]
|
||||
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml", "data/icon.png"]
|
||||
include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml", "data/icon.png"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
@@ -169,7 +169,10 @@ objc2-foundation = { workspace = true, default-features = false, features = [
|
||||
objc2-app-kit = { workspace = true, default-features = false, features = [
|
||||
"std",
|
||||
"NSApplication",
|
||||
"NSBitmapImageRep",
|
||||
"NSGraphics",
|
||||
"NSImage",
|
||||
"NSImageRep",
|
||||
"NSMenu",
|
||||
"NSMenuItem",
|
||||
"NSResponder",
|
||||
|
||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 12 KiB |
@@ -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.
|
||||
///
|
||||
|
||||
@@ -204,7 +204,7 @@ fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconS
|
||||
use crate::icon_data::IconDataExt as _;
|
||||
profiling::function_scope!();
|
||||
|
||||
use objc2::ClassType as _;
|
||||
use objc2::AnyThread as _;
|
||||
use objc2_app_kit::{NSApplication, NSImage};
|
||||
use objc2_foundation::NSString;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -34,7 +34,7 @@ use egui_winit::accesskit_winit;
|
||||
|
||||
use crate::{
|
||||
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
|
||||
native::epi_integration::EpiIntegration,
|
||||
native::{epi_integration::EpiIntegration, winit_integration::is_invisible_or_minimized},
|
||||
};
|
||||
|
||||
use super::{
|
||||
@@ -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()),
|
||||
@@ -466,12 +467,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()));
|
||||
}
|
||||
}
|
||||
@@ -554,7 +564,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 {
|
||||
@@ -565,6 +575,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);
|
||||
};
|
||||
@@ -580,7 +592,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
|
||||
@@ -596,7 +608,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();
|
||||
@@ -631,9 +643,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,
|
||||
);
|
||||
|
||||
// ------------------------------------------------------------
|
||||
|
||||
@@ -676,85 +691,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);
|
||||
@@ -763,9 +780,11 @@ impl GlowWinitRunning<'_> {
|
||||
|
||||
integration.maybe_autosave(app.as_mut(), Some(&window));
|
||||
|
||||
if window.is_minimized() == Some(true) {
|
||||
if is_invisible_or_minimized(&window) {
|
||||
// On Mac, a minimized Window uses up all CPU:
|
||||
// https://github.com/emilk/egui/issues/325
|
||||
// On Windows, an invisible window also uses up all CPU:
|
||||
// https://github.com/emilk/egui/issues/7776
|
||||
profiling::scope!("minimized_sleep");
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
@@ -831,6 +850,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!(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::time::Instant;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use winit::{
|
||||
application::ApplicationHandler,
|
||||
@@ -11,9 +11,20 @@ use ahash::HashMap;
|
||||
use super::winit_integration::{UserEvent, WinitApp};
|
||||
use crate::{
|
||||
Result, epi,
|
||||
native::{event_loop_context, winit_integration::EventResult},
|
||||
native::{
|
||||
event_loop_context,
|
||||
winit_integration::{EventResult, is_invisible_or_minimized},
|
||||
},
|
||||
};
|
||||
|
||||
/// Minimum interval between repaints for invisible windows.
|
||||
///
|
||||
/// On Windows, invisible windows don't receive `RedrawRequested` events,
|
||||
/// so we throttle their repaints to avoid busy-looping while still
|
||||
/// processing viewport commands like `Visible(true)`.
|
||||
/// See <https://github.com/emilk/egui/issues/7776>.
|
||||
const INVISIBLE_WINDOW_REPAINT_INTERVAL: Duration = Duration::from_millis(100);
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
fn create_event_loop(native_options: &mut epi::NativeOptions) -> Result<EventLoop<UserEvent>> {
|
||||
#[cfg(target_os = "android")]
|
||||
@@ -177,23 +188,54 @@ impl<T: WinitApp> WinitAppWrapper<T> {
|
||||
fn check_redraw_requests(&mut self, event_loop: &ActiveEventLoop) {
|
||||
let now = Instant::now();
|
||||
|
||||
let mut invisible_window_ids = Vec::new();
|
||||
|
||||
self.windows_next_repaint_times
|
||||
.retain(|window_id, repaint_time| {
|
||||
if now < *repaint_time {
|
||||
return true; // not yet ready
|
||||
}
|
||||
|
||||
event_loop.set_control_flow(ControlFlow::Poll);
|
||||
|
||||
if let Some(window) = self.winit_app.window(*window_id) {
|
||||
log::trace!("request_redraw for {window_id:?}");
|
||||
window.request_redraw();
|
||||
// On Windows, invisible windows don't receive RedrawRequested
|
||||
// events, so pending viewport commands (e.g. Visible(true)) would
|
||||
// never be processed. We collect these windows to paint them
|
||||
// directly below.
|
||||
// See: https://github.com/emilk/egui/issues/5229
|
||||
if is_invisible_or_minimized(&window) {
|
||||
invisible_window_ids.push(*window_id);
|
||||
} else {
|
||||
log::trace!("request_redraw for {window_id:?}");
|
||||
event_loop.set_control_flow(ControlFlow::Poll);
|
||||
window.request_redraw();
|
||||
}
|
||||
} else {
|
||||
log::trace!("No window found for {window_id:?}");
|
||||
}
|
||||
false
|
||||
});
|
||||
|
||||
// Paint invisible windows directly, since they won't receive
|
||||
// RedrawRequested events on Windows. This ensures that viewport
|
||||
// commands like Visible(true) are still processed.
|
||||
for window_id in &invisible_window_ids {
|
||||
let event_result = self.winit_app.run_ui_and_paint(event_loop, *window_id);
|
||||
self.handle_event_result(event_loop, event_result);
|
||||
}
|
||||
|
||||
// Throttle any already-scheduled repaints for invisible windows
|
||||
// to avoid busy-looping. If no repaint was requested by the app,
|
||||
// the window will simply sleep.
|
||||
// See: https://github.com/emilk/egui/issues/7776
|
||||
if !invisible_window_ids.is_empty() {
|
||||
let next_paint = Instant::now() + INVISIBLE_WINDOW_REPAINT_INTERVAL;
|
||||
for window_id in &invisible_window_ids {
|
||||
self.windows_next_repaint_times
|
||||
.entry(*window_id)
|
||||
.and_modify(|t| *t = (*t).min(next_paint));
|
||||
}
|
||||
}
|
||||
|
||||
let next_repaint_time = self.windows_next_repaint_times.values().min().copied();
|
||||
if let Some(next_repaint_time) = next_repaint_time {
|
||||
event_loop.set_control_flow(ControlFlow::WaitUntil(next_repaint_time));
|
||||
@@ -270,6 +312,16 @@ impl<T: WinitApp> ApplicationHandler<UserEvent> for WinitAppWrapper<T> {
|
||||
if let Some(window_id) =
|
||||
self.winit_app.window_id_from_viewport_id(viewport_id)
|
||||
{
|
||||
// Throttle repaints for invisible windows to prevent
|
||||
// high CPU usage on Windows.
|
||||
// See: https://github.com/emilk/egui/issues/7776
|
||||
let when = if let Some(window) = self.winit_app.window(window_id)
|
||||
&& is_invisible_or_minimized(&window)
|
||||
{
|
||||
when.max(Instant::now() + INVISIBLE_WINDOW_REPAINT_INTERVAL)
|
||||
} else {
|
||||
when
|
||||
};
|
||||
Ok(EventResult::RepaintAt(window_id, when))
|
||||
} else {
|
||||
Ok(EventResult::Wait)
|
||||
|
||||
@@ -27,7 +27,10 @@ use winit_integration::UserEvent;
|
||||
|
||||
use crate::{
|
||||
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
|
||||
native::{epi_integration::EpiIntegration, winit_integration::EventResult},
|
||||
native::{
|
||||
epi_integration::EpiIntegration,
|
||||
winit_integration::{EventResult, is_invisible_or_minimized},
|
||||
},
|
||||
};
|
||||
|
||||
use super::{epi_integration, event_loop_context, winit_integration, winit_integration::WinitApp};
|
||||
@@ -184,9 +187,17 @@ impl<'app> WgpuWinitApp<'app> {
|
||||
builder: ViewportBuilder,
|
||||
) -> crate::Result<&mut WgpuWinitRunning<'app>> {
|
||||
profiling::function_scope!();
|
||||
// Inject the display handle into the wgpu setup so that wgpu can create
|
||||
// surfaces on platforms that require it (e.g. GLES on Wayland).
|
||||
let mut wgpu_options = self.native_options.wgpu_options.clone();
|
||||
if let egui_wgpu::WgpuSetup::CreateNew(ref mut create_new) = wgpu_options.wgpu_setup
|
||||
&& create_new.display_handle.is_none()
|
||||
{
|
||||
create_new.display_handle = Some(Box::new(event_loop.owned_display_handle()));
|
||||
}
|
||||
let mut painter = pollster::block_on(egui_wgpu::winit::Painter::new(
|
||||
egui_ctx.clone(),
|
||||
self.native_options.wgpu_options.clone(),
|
||||
wgpu_options,
|
||||
self.native_options.viewport.transparent.unwrap_or(false),
|
||||
egui_wgpu::RendererOptions {
|
||||
msaa_samples: self.native_options.multisampling as _,
|
||||
@@ -474,12 +485,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()));
|
||||
}
|
||||
}
|
||||
@@ -584,7 +604,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();
|
||||
|
||||
@@ -628,6 +648,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))))?;
|
||||
@@ -648,14 +670,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,
|
||||
);
|
||||
|
||||
// ------------------------------------------------------------
|
||||
|
||||
@@ -696,52 +723,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();
|
||||
|
||||
@@ -768,10 +801,12 @@ impl WgpuWinitRunning<'_> {
|
||||
integration.maybe_autosave(app.as_mut(), window.map(|w| w.as_ref()));
|
||||
|
||||
if let Some(window) = window
|
||||
&& window.is_minimized() == Some(true)
|
||||
&& is_invisible_or_minimized(window)
|
||||
{
|
||||
// On Mac, a minimized Window uses up all CPU:
|
||||
// https://github.com/emilk/egui/issues/325
|
||||
// On Windows, an invisible window also uses up all CPU:
|
||||
// https://github.com/emilk/egui/issues/7776
|
||||
profiling::scope!("minimized_sleep");
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
@@ -863,6 +898,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!(
|
||||
|
||||
@@ -9,6 +9,14 @@ use egui::ViewportId;
|
||||
#[cfg(feature = "accesskit")]
|
||||
use egui_winit::accesskit_winit;
|
||||
|
||||
/// Returns `true` if the window is invisible or minimized.
|
||||
///
|
||||
/// These windows don't receive `RedrawRequested` events on Windows,
|
||||
/// so they need special handling to keep processing viewport commands.
|
||||
pub fn is_invisible_or_minimized(window: &Window) -> bool {
|
||||
window.is_visible() == Some(false) || window.is_minimized() == Some(true)
|
||||
}
|
||||
|
||||
/// Create an egui context, restoring it from storage if possible.
|
||||
pub fn create_egui_context(storage: Option<&dyn crate::Storage>) -> egui::Context {
|
||||
profiling::function_scope!();
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ mod web_painter_wgpu;
|
||||
pub use backend::*;
|
||||
|
||||
use egui::Theme;
|
||||
use js_sys::Object;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use web_sys::{Document, MediaQueryList, Node};
|
||||
|
||||
@@ -370,5 +371,5 @@ pub fn percent_decode(s: &str) -> String {
|
||||
|
||||
/// Are we running inside the Safari browser?
|
||||
pub fn is_safari_browser() -> bool {
|
||||
web_sys::window().is_some_and(|window| window.has_own_property(&JsValue::from("safari")))
|
||||
web_sys::window().is_some_and(|window| Object::has_own(&window, &JsValue::from("safari")))
|
||||
}
|
||||
|
||||
@@ -15,13 +15,32 @@ pub(crate) struct WebPainterWgpu {
|
||||
surface: wgpu::Surface<'static>,
|
||||
surface_configuration: wgpu::SurfaceConfiguration,
|
||||
render_state: Option<RenderState>,
|
||||
on_surface_error: Arc<dyn Fn(wgpu::SurfaceError) -> SurfaceErrorAction>,
|
||||
on_surface_status: Arc<dyn Fn(&wgpu::CurrentSurfaceTexture) -> SurfaceErrorAction>,
|
||||
depth_stencil_format: Option<wgpu::TextureFormat>,
|
||||
depth_texture_view: Option<wgpu::TextureView>,
|
||||
screen_capture_state: Option<CaptureState>,
|
||||
capture_tx: CaptureSender,
|
||||
capture_rx: CaptureReceiver,
|
||||
ctx: egui::Context,
|
||||
needs_reconfigure: bool,
|
||||
}
|
||||
|
||||
/// Owned web display handle that is `Send + Sync`.
|
||||
///
|
||||
/// `DisplayHandle` from `raw-window-handle` is `!Send`/`!Sync` because the enum
|
||||
/// contains platform variants with raw pointers. On web the handle is always empty,
|
||||
/// so this wrapper is safe.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[derive(Clone, Debug)]
|
||||
struct WebDisplay;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
impl egui_wgpu::wgpu::rwh::HasDisplayHandle for WebDisplay {
|
||||
fn display_handle(
|
||||
&self,
|
||||
) -> Result<egui_wgpu::wgpu::rwh::DisplayHandle<'_>, egui_wgpu::wgpu::rwh::HandleError> {
|
||||
Ok(egui_wgpu::wgpu::rwh::DisplayHandle::web())
|
||||
}
|
||||
}
|
||||
|
||||
impl WebPainterWgpu {
|
||||
@@ -63,7 +82,17 @@ impl WebPainterWgpu {
|
||||
) -> Result<Self, String> {
|
||||
log::debug!("Creating wgpu painter");
|
||||
|
||||
let instance = options.wgpu_options.wgpu_setup.new_instance().await;
|
||||
// Inject the display handle into the wgpu setup so that wgpu can create surfaces on WebGL.
|
||||
let mut wgpu_options = options.wgpu_options.clone();
|
||||
if let egui_wgpu::WgpuSetup::CreateNew(ref mut create_new) = wgpu_options.wgpu_setup
|
||||
&& create_new.display_handle.is_none()
|
||||
{
|
||||
// Force WebGL, useful for quick & dirty testing:
|
||||
//create_new.instance_descriptor.backends = wgpu::Backends::GL;
|
||||
create_new.display_handle = Some(Box::new(WebDisplay));
|
||||
}
|
||||
|
||||
let instance = wgpu_options.wgpu_setup.new_instance().await;
|
||||
let surface = instance
|
||||
.create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone()))
|
||||
.map_err(|err| format!("failed to create wgpu surface: {err}"))?;
|
||||
@@ -71,7 +100,7 @@ impl WebPainterWgpu {
|
||||
let depth_stencil_format = egui_wgpu::depth_format_from_bits(options.depth_buffer, 0);
|
||||
|
||||
let render_state = RenderState::create(
|
||||
&options.wgpu_options,
|
||||
&wgpu_options,
|
||||
&instance,
|
||||
Some(&surface),
|
||||
egui_wgpu::RendererOptions {
|
||||
@@ -89,7 +118,7 @@ impl WebPainterWgpu {
|
||||
|
||||
let surface_configuration = wgpu::SurfaceConfiguration {
|
||||
format: render_state.target_format,
|
||||
present_mode: options.wgpu_options.present_mode,
|
||||
present_mode: wgpu_options.present_mode,
|
||||
view_formats: vec![render_state.target_format],
|
||||
..default_configuration
|
||||
};
|
||||
@@ -105,11 +134,12 @@ impl WebPainterWgpu {
|
||||
surface_configuration,
|
||||
depth_stencil_format,
|
||||
depth_texture_view: None,
|
||||
on_surface_error: Arc::clone(&options.wgpu_options.on_surface_error) as _,
|
||||
on_surface_status: Arc::clone(&wgpu_options.on_surface_status) as _,
|
||||
screen_capture_state: None,
|
||||
capture_tx,
|
||||
capture_rx,
|
||||
ctx,
|
||||
needs_reconfigure: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -195,18 +225,28 @@ impl WebPainter for WebPainterWgpu {
|
||||
);
|
||||
}
|
||||
|
||||
if self.needs_reconfigure {
|
||||
self.surface
|
||||
.configure(&render_state.device, &self.surface_configuration);
|
||||
self.needs_reconfigure = false;
|
||||
}
|
||||
|
||||
let output_frame = match self.surface.get_current_texture() {
|
||||
Ok(frame) => frame,
|
||||
Err(err) => match (*self.on_surface_error)(err) {
|
||||
SurfaceErrorAction::RecreateSurface => {
|
||||
self.surface
|
||||
.configure(&render_state.device, &self.surface_configuration);
|
||||
return Ok(());
|
||||
wgpu::CurrentSurfaceTexture::Success(frame) => frame,
|
||||
wgpu::CurrentSurfaceTexture::Suboptimal(frame) => {
|
||||
self.needs_reconfigure = true;
|
||||
frame
|
||||
}
|
||||
other => {
|
||||
match (*self.on_surface_status)(&other) {
|
||||
SurfaceErrorAction::RecreateSurface => {
|
||||
self.surface
|
||||
.configure(&render_state.device, &self.surface_configuration);
|
||||
}
|
||||
SurfaceErrorAction::SkipFrame => {}
|
||||
}
|
||||
SurfaceErrorAction::SkipFrame => {
|
||||
return Ok(());
|
||||
}
|
||||
},
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
{
|
||||
@@ -268,6 +308,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 +321,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))
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ readme = "README.md"
|
||||
repository = "https://github.com/emilk/egui/tree/main/crates/egui-wgpu"
|
||||
categories = ["gui", "game-development"]
|
||||
keywords = ["wgpu", "egui", "gui", "gamedev"]
|
||||
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "**/*.wgsl", "Cargo.toml"]
|
||||
include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "**/*.wgsl", "Cargo.toml"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -24,7 +24,10 @@ mod renderer;
|
||||
mod setup;
|
||||
|
||||
pub use renderer::*;
|
||||
pub use setup::{NativeAdapterSelectorMethod, WgpuSetup, WgpuSetupCreateNew, WgpuSetupExisting};
|
||||
pub use setup::{
|
||||
EguiDisplayHandle, NativeAdapterSelectorMethod, WgpuSetup, WgpuSetupCreateNew,
|
||||
WgpuSetupExisting,
|
||||
};
|
||||
|
||||
/// Helpers for capturing screenshots of the UI.
|
||||
#[cfg(feature = "capture")]
|
||||
@@ -185,12 +188,13 @@ impl RenderState {
|
||||
wgpu::Backends::all()
|
||||
};
|
||||
|
||||
instance.enumerate_adapters(backends)
|
||||
instance.enumerate_adapters(backends).await
|
||||
};
|
||||
|
||||
let (adapter, device, queue) = match config.wgpu_setup.clone() {
|
||||
WgpuSetup::CreateNew(WgpuSetupCreateNew {
|
||||
instance_descriptor: _,
|
||||
display_handle: _,
|
||||
power_preference,
|
||||
native_adapter_selector: _native_adapter_selector,
|
||||
device_descriptor,
|
||||
@@ -272,7 +276,7 @@ fn describe_adapters(adapters: &[wgpu::Adapter]) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Specifies which action should be taken as consequence of a [`wgpu::SurfaceError`]
|
||||
/// Specifies which action should be taken as consequence of a surface error.
|
||||
pub enum SurfaceErrorAction {
|
||||
/// Do nothing and skip the current frame.
|
||||
SkipFrame,
|
||||
@@ -299,8 +303,15 @@ pub struct WgpuConfiguration {
|
||||
/// How to create the wgpu adapter & device
|
||||
pub wgpu_setup: WgpuSetup,
|
||||
|
||||
/// Callback for surface errors.
|
||||
pub on_surface_error: Arc<dyn Fn(wgpu::SurfaceError) -> SurfaceErrorAction + Send + Sync>,
|
||||
/// Callback for surface status changes.
|
||||
///
|
||||
/// Called with the [`wgpu::CurrentSurfaceTexture`] result whenever acquiring a frame
|
||||
/// does not return [`wgpu::CurrentSurfaceTexture::Success`]. For
|
||||
/// [`wgpu::CurrentSurfaceTexture::Suboptimal`], egui uses the frame as-is and
|
||||
/// defers surface reconfiguration to the next frame — the callback is not invoked
|
||||
/// in that case either.
|
||||
pub on_surface_status:
|
||||
Arc<dyn Fn(&wgpu::CurrentSurfaceTexture) -> SurfaceErrorAction + Send + Sync>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -315,7 +326,7 @@ impl std::fmt::Debug for WgpuConfiguration {
|
||||
present_mode,
|
||||
desired_maximum_frame_latency,
|
||||
wgpu_setup,
|
||||
on_surface_error: _,
|
||||
on_surface_status: _,
|
||||
} = self;
|
||||
f.debug_struct("WgpuConfiguration")
|
||||
.field("present_mode", &present_mode)
|
||||
@@ -333,15 +344,25 @@ impl Default for WgpuConfiguration {
|
||||
Self {
|
||||
present_mode: wgpu::PresentMode::AutoVsync,
|
||||
desired_maximum_frame_latency: None,
|
||||
wgpu_setup: Default::default(),
|
||||
on_surface_error: Arc::new(|err| {
|
||||
if err == wgpu::SurfaceError::Outdated {
|
||||
// This error occurs when the app is minimized on Windows.
|
||||
// Silently return here to prevent spamming the console with:
|
||||
// "The underlying surface has changed, and therefore the swap chain must be updated"
|
||||
} else {
|
||||
log::warn!("Dropped frame with error: {err}");
|
||||
// No display handle available at this point — callers should replace this with
|
||||
// `WgpuSetup::from_display_handle(...)` before creating the instance if one is available.
|
||||
wgpu_setup: WgpuSetup::without_display_handle(),
|
||||
on_surface_status: Arc::new(|status| {
|
||||
match status {
|
||||
wgpu::CurrentSurfaceTexture::Outdated => {
|
||||
// This error occurs when the app is minimized on Windows.
|
||||
// Silently return here to prevent spamming the console with:
|
||||
// "The underlying surface has changed, and therefore the swap chain must be updated"
|
||||
}
|
||||
wgpu::CurrentSurfaceTexture::Occluded => {
|
||||
// This error occurs when the application is occluded (e.g. minimized or behind another window).
|
||||
log::debug!("Dropped frame with error: {status:?}");
|
||||
}
|
||||
_ => {
|
||||
log::warn!("Dropped frame with error: {status:?}");
|
||||
}
|
||||
}
|
||||
|
||||
SurfaceErrorAction::SkipFrame
|
||||
}),
|
||||
}
|
||||
@@ -395,6 +416,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 +451,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
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#![expect(clippy::unwrap_used)] // TODO(emilk): avoid unwraps
|
||||
|
||||
use std::{borrow::Cow, num::NonZeroU64, ops::Range};
|
||||
|
||||
use ahash::HashMap;
|
||||
@@ -352,16 +350,19 @@ 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: &[],
|
||||
bind_group_layouts: &[
|
||||
Some(&uniform_bind_group_layout),
|
||||
Some(&texture_bind_group_layout),
|
||||
],
|
||||
immediate_size: 0,
|
||||
});
|
||||
|
||||
let depth_stencil = options
|
||||
.depth_stencil_format
|
||||
.map(|format| wgpu::DepthStencilState {
|
||||
format,
|
||||
depth_write_enabled: false,
|
||||
depth_compare: wgpu::CompareFunction::Always,
|
||||
depth_write_enabled: Some(false),
|
||||
depth_compare: Some(wgpu::CompareFunction::Always),
|
||||
stencil: wgpu::StencilState::default(),
|
||||
bias: wgpu::DepthBiasState::default(),
|
||||
});
|
||||
@@ -426,7 +427,7 @@ impl Renderer {
|
||||
})],
|
||||
compilation_options: wgpu::PipelineCompilationOptions::default()
|
||||
}),
|
||||
multiview: None,
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
}
|
||||
)
|
||||
@@ -469,6 +470,9 @@ impl Renderer {
|
||||
/// The render pass internally keeps all referenced resources alive as long as necessary.
|
||||
/// The only consequence of `forget_lifetime` is that any operation on the parent encoder will cause a runtime error
|
||||
/// instead of a compile time error.
|
||||
///
|
||||
/// # Panic
|
||||
/// Always ensure that [`Renderer::update_buffers`] has been called otherwise calling [`Renderer::render`] will panic!
|
||||
pub fn render(
|
||||
&self,
|
||||
render_pass: &mut wgpu::RenderPass<'static>,
|
||||
@@ -513,8 +517,12 @@ impl Renderer {
|
||||
// Skip rendering zero-sized clip areas.
|
||||
if let Primitive::Mesh(_) = primitive {
|
||||
// If this is a mesh, we need to advance the index and vertex buffer iterators:
|
||||
index_buffer_slices.next().unwrap();
|
||||
vertex_buffer_slices.next().unwrap();
|
||||
index_buffer_slices
|
||||
.next()
|
||||
.expect("You must call .update_buffers() before .render()");
|
||||
vertex_buffer_slices
|
||||
.next()
|
||||
.expect("You must call .update_buffers() before .render()");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -524,8 +532,12 @@ impl Renderer {
|
||||
|
||||
match primitive {
|
||||
Primitive::Mesh(mesh) => {
|
||||
let index_buffer_slice = index_buffer_slices.next().unwrap();
|
||||
let vertex_buffer_slice = vertex_buffer_slices.next().unwrap();
|
||||
let index_buffer_slice = index_buffer_slices
|
||||
.next()
|
||||
.expect("You must call .update_buffers() before .render()");
|
||||
let vertex_buffer_slice = vertex_buffer_slices
|
||||
.next()
|
||||
.expect("You must call .update_buffers() before .render()");
|
||||
|
||||
if let Some(Texture { bind_group, .. }) = self.textures.get(&mesh.texture_id) {
|
||||
render_pass.set_bind_group(1, bind_group, &[]);
|
||||
@@ -951,6 +963,7 @@ impl Renderer {
|
||||
let index_buffer_staging = queue.write_buffer_with(
|
||||
&self.index_buffer.buffer,
|
||||
0,
|
||||
#[expect(clippy::unwrap_used)] // Checked above
|
||||
NonZeroU64::new(required_index_buffer_size).unwrap(),
|
||||
);
|
||||
|
||||
@@ -968,7 +981,8 @@ impl Renderer {
|
||||
Primitive::Mesh(mesh) => {
|
||||
let size = mesh.indices.len() * std::mem::size_of::<u32>();
|
||||
let slice = index_offset..(size + index_offset);
|
||||
index_buffer_staging[slice.clone()]
|
||||
index_buffer_staging
|
||||
.slice(slice.clone())
|
||||
.copy_from_slice(bytemuck::cast_slice(&mesh.indices));
|
||||
self.index_buffer.slices.push(slice);
|
||||
index_offset += size;
|
||||
@@ -994,6 +1008,7 @@ impl Renderer {
|
||||
let vertex_buffer_staging = queue.write_buffer_with(
|
||||
&self.vertex_buffer.buffer,
|
||||
0,
|
||||
#[expect(clippy::unwrap_used)] // Checked above
|
||||
NonZeroU64::new(required_vertex_buffer_size).unwrap(),
|
||||
);
|
||||
|
||||
@@ -1011,7 +1026,8 @@ impl Renderer {
|
||||
Primitive::Mesh(mesh) => {
|
||||
let size = mesh.vertices.len() * std::mem::size_of::<Vertex>();
|
||||
let slice = vertex_offset..(size + vertex_offset);
|
||||
vertex_buffer_staging[slice.clone()]
|
||||
vertex_buffer_staging
|
||||
.slice(slice.clone())
|
||||
.copy_from_slice(bytemuck::cast_slice(&mesh.vertices));
|
||||
self.vertex_buffer.slices.push(slice);
|
||||
vertex_offset += size;
|
||||
|
||||
@@ -1,5 +1,43 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A cloneable display handle for use with [`wgpu::InstanceDescriptor`].
|
||||
///
|
||||
/// [`wgpu::InstanceDescriptor`] stores its display handle as a non-cloneable
|
||||
/// `Box<dyn WgpuHasDisplayHandle>`. This trait wraps it so it can be cloned
|
||||
/// alongside the rest of the egui wgpu configuration.
|
||||
///
|
||||
/// Automatically implemented for all types that satisfy the bounds
|
||||
/// (including [`winit::event_loop::OwnedDisplayHandle`]).
|
||||
pub trait EguiDisplayHandle:
|
||||
wgpu::rwh::HasDisplayHandle + std::fmt::Debug + Send + Sync + 'static
|
||||
{
|
||||
/// Clone into a `Box<dyn WgpuHasDisplayHandle>` for [`wgpu::InstanceDescriptor::display`].
|
||||
fn clone_for_wgpu(&self) -> Box<dyn wgpu::wgt::WgpuHasDisplayHandle>;
|
||||
|
||||
/// Clone into a new `Box<dyn EguiDisplayHandle>`.
|
||||
fn clone_display_handle(&self) -> Box<dyn EguiDisplayHandle>;
|
||||
}
|
||||
|
||||
impl Clone for Box<dyn EguiDisplayHandle> {
|
||||
fn clone(&self) -> Self {
|
||||
// We need to deref here, otherwise this causes infinite recursion stack overflow.
|
||||
(**self).clone_display_handle()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> EguiDisplayHandle for T
|
||||
where
|
||||
T: wgpu::rwh::HasDisplayHandle + Clone + std::fmt::Debug + Send + Sync + 'static,
|
||||
{
|
||||
fn clone_for_wgpu(&self) -> Box<dyn wgpu::wgt::WgpuHasDisplayHandle> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
|
||||
fn clone_display_handle(&self) -> Box<dyn EguiDisplayHandle> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum WgpuSetup {
|
||||
/// Construct a wgpu setup using some predefined settings & heuristics.
|
||||
@@ -22,9 +60,19 @@ pub enum WgpuSetup {
|
||||
Existing(WgpuSetupExisting),
|
||||
}
|
||||
|
||||
impl Default for WgpuSetup {
|
||||
fn default() -> Self {
|
||||
Self::CreateNew(WgpuSetupCreateNew::default())
|
||||
impl WgpuSetup {
|
||||
/// Creates a new [`WgpuSetup::CreateNew`] with the given display handle.
|
||||
///
|
||||
/// See [`WgpuSetupCreateNew::from_display_handle`] for details.
|
||||
pub fn from_display_handle(display_handle: impl EguiDisplayHandle) -> Self {
|
||||
Self::CreateNew(WgpuSetupCreateNew::from_display_handle(display_handle))
|
||||
}
|
||||
|
||||
/// Creates a new [`WgpuSetup::CreateNew`] without a display handle.
|
||||
///
|
||||
/// See [`WgpuSetupCreateNew::without_display_handle`] for details.
|
||||
pub fn without_display_handle() -> Self {
|
||||
Self::CreateNew(WgpuSetupCreateNew::without_display_handle())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,8 +113,18 @@ impl WgpuSetup {
|
||||
}
|
||||
|
||||
log::debug!("Creating wgpu instance with backends {backends:?}");
|
||||
wgpu::util::new_instance_with_webgpu_detection(&create_new.instance_descriptor)
|
||||
.await
|
||||
let desc = &create_new.instance_descriptor;
|
||||
let descriptor = wgpu::InstanceDescriptor {
|
||||
backends: desc.backends,
|
||||
flags: desc.flags,
|
||||
backend_options: desc.backend_options.clone(),
|
||||
memory_budget_thresholds: desc.memory_budget_thresholds,
|
||||
display: create_new
|
||||
.display_handle
|
||||
.as_ref()
|
||||
.map(|handle| handle.clone_for_wgpu()),
|
||||
};
|
||||
wgpu::util::new_instance_with_webgpu_detection(descriptor).await
|
||||
}
|
||||
Self::Existing(existing) => existing.instance.clone(),
|
||||
}
|
||||
@@ -98,18 +156,35 @@ pub type NativeAdapterSelectorMethod = Arc<
|
||||
/// Configuration for creating a new wgpu setup.
|
||||
///
|
||||
/// Used for [`WgpuSetup::CreateNew`].
|
||||
///
|
||||
/// Prefer [`Self::from_display_handle`] when you have a display handle available.
|
||||
/// Most platforms work without one, but some (e.g. Wayland with GLES, or WebGL)
|
||||
/// require it, so providing one ensures maximum compatibility.
|
||||
/// With winit, pass [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle).
|
||||
///
|
||||
/// Note: The display handle is stored in [`Self::display_handle`] rather than in
|
||||
/// [`Self::instance_descriptor`] so the config can be cloned
|
||||
/// ([`wgpu::InstanceDescriptor`] is not `Clone`). It is injected at instance creation time.
|
||||
pub struct WgpuSetupCreateNew {
|
||||
/// Instance descriptor for creating a wgpu instance.
|
||||
/// Descriptor for the wgpu instance.
|
||||
///
|
||||
/// The most important field is [`wgpu::InstanceDescriptor::backends`], which
|
||||
/// controls which backends are supported (wgpu will pick one of these).
|
||||
/// If you only want to support WebGL (and not WebGPU),
|
||||
/// you can set this to [`wgpu::Backends::GL`].
|
||||
/// By default on web, WebGPU will be used if available.
|
||||
/// WebGL will only be used as a fallback,
|
||||
/// and only if you have enabled the `webgl` feature of crate `wgpu`.
|
||||
/// Leave [`wgpu::InstanceDescriptor::display`] as `None` — use [`Self::display_handle`]
|
||||
/// instead (injected at instance creation time).
|
||||
///
|
||||
/// The most important field is [`wgpu::InstanceDescriptor::backends`], which controls
|
||||
/// which backends are supported (wgpu will pick one of these). For example, set it to
|
||||
/// [`wgpu::Backends::GL`] to use only WebGL. By default on web, WebGPU is preferred
|
||||
/// with WebGL as a fallback (requires the `webgl` feature of crate `wgpu`).
|
||||
pub instance_descriptor: wgpu::InstanceDescriptor,
|
||||
|
||||
/// Display handle passed to wgpu at instance creation time.
|
||||
///
|
||||
/// Required on some platforms (e.g. Wayland with GLES, WebGL); optional elsewhere.
|
||||
/// With winit, use [`winit::event_loop::OwnedDisplayHandle`].
|
||||
///
|
||||
/// `eframe` 's winit & web integrations will attempt to fill the display handle automatically if it is left empty.
|
||||
pub display_handle: Option<Box<dyn EguiDisplayHandle>>,
|
||||
|
||||
/// Power preference for the adapter if [`Self::native_adapter_selector`] is not set or targeting web.
|
||||
pub power_preference: wgpu::PowerPreference,
|
||||
|
||||
@@ -128,32 +203,37 @@ pub struct WgpuSetupCreateNew {
|
||||
Arc<dyn Fn(&wgpu::Adapter) -> wgpu::DeviceDescriptor<'static> + Send + Sync>,
|
||||
}
|
||||
|
||||
impl Clone for WgpuSetupCreateNew {
|
||||
fn clone(&self) -> Self {
|
||||
impl WgpuSetupCreateNew {
|
||||
/// Creates a new configuration with the given display handle.
|
||||
///
|
||||
/// This is the recommended constructor. Most platforms (Windows, macOS/iOS, Android, web)
|
||||
/// work fine without a display handle, but some (e.g. Wayland on Linux with GLES) require
|
||||
/// one. Providing it unconditionally ensures your app works everywhere.
|
||||
///
|
||||
/// If you don't have a display handle available, use [`Self::without_display_handle`]
|
||||
/// instead — it will still work on the majority of platforms.
|
||||
///
|
||||
/// With winit, pass [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle).
|
||||
pub fn from_display_handle(display_handle: impl EguiDisplayHandle) -> Self {
|
||||
Self {
|
||||
instance_descriptor: self.instance_descriptor.clone(),
|
||||
power_preference: self.power_preference,
|
||||
native_adapter_selector: self.native_adapter_selector.clone(),
|
||||
device_descriptor: Arc::clone(&self.device_descriptor),
|
||||
display_handle: Some(Box::new(display_handle)),
|
||||
..Self::without_display_handle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for WgpuSetupCreateNew {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("WgpuSetupCreateNew")
|
||||
.field("instance_descriptor", &self.instance_descriptor)
|
||||
.field("power_preference", &self.power_preference)
|
||||
.field(
|
||||
"native_adapter_selector",
|
||||
&self.native_adapter_selector.is_some(),
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for WgpuSetupCreateNew {
|
||||
fn default() -> Self {
|
||||
/// Creates a new configuration without a display handle.
|
||||
///
|
||||
/// A display handle is not required for headless operation (offscreen rendering, tests,
|
||||
/// compute-only workloads). It also isn't needed on most platforms even when presenting
|
||||
/// to a window — only some configurations (e.g. Wayland on Linux with GLES) require one.
|
||||
///
|
||||
/// If you do have a display handle available, prefer [`Self::from_display_handle`] for
|
||||
/// maximum compatibility.
|
||||
///
|
||||
/// With winit you can obtain one via [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle).
|
||||
///
|
||||
/// `eframe` 's winit & web integrations will attempt to fill the display handle automatically if it is left empty.
|
||||
pub fn without_display_handle() -> Self {
|
||||
Self {
|
||||
instance_descriptor: wgpu::InstanceDescriptor {
|
||||
// Add GL backend, primarily because WebGPU is not stable enough yet.
|
||||
@@ -163,8 +243,11 @@ impl Default for WgpuSetupCreateNew {
|
||||
flags: wgpu::InstanceFlags::from_build_config().with_env(),
|
||||
backend_options: wgpu::BackendOptions::from_env_or_default(),
|
||||
memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
|
||||
display: None,
|
||||
},
|
||||
|
||||
display_handle: None,
|
||||
|
||||
power_preference: wgpu::PowerPreference::from_env()
|
||||
.unwrap_or(wgpu::PowerPreference::HighPerformance),
|
||||
|
||||
@@ -192,6 +275,39 @@ impl Default for WgpuSetupCreateNew {
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for WgpuSetupCreateNew {
|
||||
fn clone(&self) -> Self {
|
||||
let desc = &self.instance_descriptor;
|
||||
Self {
|
||||
instance_descriptor: wgpu::InstanceDescriptor {
|
||||
backends: desc.backends,
|
||||
flags: desc.flags,
|
||||
backend_options: desc.backend_options.clone(),
|
||||
memory_budget_thresholds: desc.memory_budget_thresholds,
|
||||
display: None,
|
||||
},
|
||||
display_handle: self.display_handle.clone(),
|
||||
power_preference: self.power_preference,
|
||||
native_adapter_selector: self.native_adapter_selector.clone(),
|
||||
device_descriptor: Arc::clone(&self.device_descriptor),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for WgpuSetupCreateNew {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("WgpuSetupCreateNew")
|
||||
.field("instance_descriptor", &self.instance_descriptor)
|
||||
.field("display_handle", &self.display_handle)
|
||||
.field("power_preference", &self.power_preference)
|
||||
.field(
|
||||
"native_adapter_selector",
|
||||
&self.native_adapter_selector.is_some(),
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for using an existing wgpu setup.
|
||||
///
|
||||
/// Used for [`WgpuSetup::Existing`].
|
||||
|
||||
@@ -17,6 +17,7 @@ struct SurfaceState {
|
||||
width: u32,
|
||||
height: u32,
|
||||
resizing: bool,
|
||||
needs_reconfigure: bool,
|
||||
}
|
||||
|
||||
/// Everything you need to paint egui with [`wgpu`] on [`winit`].
|
||||
@@ -234,6 +235,7 @@ impl Painter {
|
||||
height: size.height,
|
||||
alpha_mode,
|
||||
resizing: false,
|
||||
needs_reconfigure: false,
|
||||
},
|
||||
);
|
||||
let Some(width) = NonZeroU32::new(size.width) else {
|
||||
@@ -362,14 +364,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()
|
||||
.setPresentsWithTransaction(resizing);
|
||||
|
||||
Self::configure_surface(
|
||||
state,
|
||||
@@ -421,13 +422,41 @@ 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 Some(surface_state) = self.surfaces.get(&viewport_id) else {
|
||||
|
||||
let mut render_queue_guard = RendererQueueGuard {
|
||||
queue: &render_state.queue,
|
||||
commands_submitted: false,
|
||||
};
|
||||
|
||||
let Some(surface_state) = self.surfaces.get_mut(&viewport_id) else {
|
||||
return vsync_sec;
|
||||
};
|
||||
|
||||
@@ -464,6 +493,11 @@ impl Painter {
|
||||
)
|
||||
};
|
||||
|
||||
if surface_state.needs_reconfigure {
|
||||
Self::configure_surface(surface_state, render_state, &self.configuration);
|
||||
surface_state.needs_reconfigure = false;
|
||||
}
|
||||
|
||||
let output_frame = {
|
||||
profiling::scope!("get_current_texture");
|
||||
// This is what vsync-waiting happens on my Mac.
|
||||
@@ -474,16 +508,20 @@ impl Painter {
|
||||
};
|
||||
|
||||
let output_frame = match output_frame {
|
||||
Ok(frame) => frame,
|
||||
Err(err) => match (*self.configuration.on_surface_error)(err) {
|
||||
SurfaceErrorAction::RecreateSurface => {
|
||||
Self::configure_surface(surface_state, render_state, &self.configuration);
|
||||
return vsync_sec;
|
||||
wgpu::CurrentSurfaceTexture::Success(frame) => frame,
|
||||
wgpu::CurrentSurfaceTexture::Suboptimal(frame) => {
|
||||
surface_state.needs_reconfigure = true;
|
||||
frame
|
||||
}
|
||||
other => {
|
||||
match (*self.configuration.on_surface_status)(&other) {
|
||||
SurfaceErrorAction::RecreateSurface => {
|
||||
Self::configure_surface(surface_state, render_state, &self.configuration);
|
||||
}
|
||||
SurfaceErrorAction::SkipFrame => {}
|
||||
}
|
||||
SurfaceErrorAction::SkipFrame => {
|
||||
return vsync_sec;
|
||||
}
|
||||
},
|
||||
return vsync_sec;
|
||||
}
|
||||
};
|
||||
|
||||
let mut capture_buffer = None;
|
||||
@@ -554,6 +592,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 +629,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.
|
||||
|
||||
@@ -11,7 +11,7 @@ readme = "README.md"
|
||||
repository = "https://github.com/emilk/egui/tree/main/crates/egui-winit"
|
||||
categories = ["gui", "game-development"]
|
||||
keywords = ["winit", "egui", "gui", "gamedev"]
|
||||
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
|
||||
include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -81,6 +81,7 @@ objc2.workspace = true
|
||||
objc2-foundation = { workspace = true, features = ["std", "NSThread"] }
|
||||
objc2-ui-kit = { workspace = true, features = [
|
||||
"std",
|
||||
"objc2-core-foundation",
|
||||
"UIApplication",
|
||||
"UIGeometry",
|
||||
"UIResponder",
|
||||
|
||||
@@ -65,13 +65,12 @@ impl Clipboard {
|
||||
feature = "smithay-clipboard"
|
||||
))]
|
||||
if let Some(clipboard) = &mut self.smithay {
|
||||
return match clipboard.load() {
|
||||
Ok(text) => Some(text),
|
||||
match clipboard.load() {
|
||||
Ok(text) => return Some(text),
|
||||
Err(err) => {
|
||||
log::error!("smithay paste error: {err}");
|
||||
None
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
|
||||
#![expect(clippy::manual_range_contains)]
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub use accesskit_winit;
|
||||
pub use egui;
|
||||
@@ -102,10 +105,16 @@ 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>,
|
||||
|
||||
/// Used by [`State::try_on_ime_processed_keyboard_input`] to track key
|
||||
/// release events that should be filtered out. See comments in that method
|
||||
/// for details.
|
||||
#[cfg(target_os = "windows")]
|
||||
pressed_processed_physical_keys: HashSet<winit::keyboard::PhysicalKey>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
@@ -148,6 +157,8 @@ impl State {
|
||||
|
||||
allow_ime: false,
|
||||
ime_rect_px: None,
|
||||
#[cfg(target_os = "windows")]
|
||||
pressed_processed_physical_keys: HashSet::new(),
|
||||
};
|
||||
|
||||
slf.egui_input
|
||||
@@ -364,25 +375,33 @@ impl State {
|
||||
is_synthetic,
|
||||
..
|
||||
} => {
|
||||
// Winit generates fake "synthetic" KeyboardInput events when the focus
|
||||
// is changed to the window, or away from it. Synthetic key presses
|
||||
// represent no real key presses and should be ignored.
|
||||
// See https://github.com/rust-windowing/winit/issues/3543
|
||||
if *is_synthetic && event.state == ElementState::Pressed {
|
||||
// Winit generates fake "synthetic" KeyboardInput events when the focus
|
||||
// is changed to the window, or away from it. Synthetic key presses
|
||||
// represent no real key presses and should be ignored.
|
||||
// See https://github.com/rust-windowing/winit/issues/3543
|
||||
EventResponse {
|
||||
repaint: true,
|
||||
consumed: false,
|
||||
}
|
||||
} else {
|
||||
self.on_keyboard_input(event);
|
||||
let egui_wants_keyboard_input = self.egui_ctx.egui_wants_keyboard_input();
|
||||
|
||||
// When pressing the Tab key, egui focuses the first focusable element, hence Tab always consumes.
|
||||
let consumed = self.egui_ctx.egui_wants_keyboard_input()
|
||||
|| event.logical_key
|
||||
== winit::keyboard::Key::Named(winit::keyboard::NamedKey::Tab);
|
||||
EventResponse {
|
||||
repaint: true,
|
||||
consumed,
|
||||
if let Some(response) =
|
||||
self.try_on_ime_processed_keyboard_input(event, egui_wants_keyboard_input)
|
||||
{
|
||||
response
|
||||
} else {
|
||||
self.on_keyboard_input(event);
|
||||
|
||||
// When pressing the Tab key, egui focuses the first focusable element, hence Tab always consumes.
|
||||
let consumed = egui_wants_keyboard_input
|
||||
|| event.logical_key
|
||||
== winit::keyboard::Key::Named(winit::keyboard::NamedKey::Tab);
|
||||
EventResponse {
|
||||
repaint: true,
|
||||
consumed,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -526,6 +545,91 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
#[expect(clippy::unused_self, clippy::needless_pass_by_ref_mut)]
|
||||
#[inline(always)]
|
||||
fn try_on_ime_processed_keyboard_input(
|
||||
&mut self,
|
||||
_event: &winit::event::KeyEvent,
|
||||
_egui_wants_keyboard_input: bool,
|
||||
) -> Option<EventResponse> {
|
||||
// `KeyboardInput` events processed by the IME are not emitted by
|
||||
// `winit` on non-Windows platforms, so we don't need to do anything
|
||||
// here.
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[inline(always)]
|
||||
fn try_on_ime_processed_keyboard_input(
|
||||
&mut self,
|
||||
event: &winit::event::KeyEvent,
|
||||
egui_wants_keyboard_input: bool,
|
||||
) -> Option<EventResponse> {
|
||||
if !self.allow_ime {
|
||||
None
|
||||
} else if event.logical_key == winit::keyboard::NamedKey::Process {
|
||||
// On Windows, the current version of `winit` (0.30.12) has a bug
|
||||
// where `KeyboardInput` events processed by the IME are still
|
||||
// emitted. [^1]
|
||||
//
|
||||
// As a workaround, we detect these events by checking whether their
|
||||
// `logical_key` is `winit::keyboard::NamedKey::Process`, and filter
|
||||
// them out to keep behavior consistent with other platforms.
|
||||
//
|
||||
// `winit::keyboard::NamedKey::Process` is not documented in
|
||||
// `winit`. Reading through its source code, we find that it is
|
||||
// mapped from `VK_PROCESSKEY` on Windows [^2]. (On an unrelated
|
||||
// note, Web is the only other platform that also uses it [^3].)
|
||||
// According to Microsoft, “the IME sets the virtual key value
|
||||
// to `VK_PROCESSKEY` after processing a key input message” [^4].
|
||||
// See also [^5].
|
||||
// (I can't find a documentation page dedicated to this value.)
|
||||
//
|
||||
// TODO(umajho): Remove this workaround once the `winit` bug is fixed
|
||||
// and we've updated to a version that includes the fix. NOTE: Don't
|
||||
// forget to also remove the `pressed_processed_physical_keys` field
|
||||
// and its related code.
|
||||
//
|
||||
// [^1]: https://github.com/rust-windowing/winit/issues/4508
|
||||
// [^2]: https://github.com/rust-windowing/winit/blob/e9809ef54b18499bb4f2cac945719ecc2a61061b/src/platform_impl/windows/keyboard_layout.rs#L946
|
||||
// [^3]: https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values
|
||||
// [^4]: https://learn.microsoft.com/en-us/windows/win32/api/imm/nf-imm-immgetvirtualkey#remarks
|
||||
// [^5]: https://learn.microsoft.com/en-us/windows/win32/learnwin32/keyboard-input#character-messages
|
||||
|
||||
self.pressed_processed_physical_keys
|
||||
.insert(event.physical_key);
|
||||
|
||||
Some(EventResponse {
|
||||
repaint: false,
|
||||
consumed: egui_wants_keyboard_input,
|
||||
})
|
||||
} else if event.state == ElementState::Released
|
||||
&& self
|
||||
.pressed_processed_physical_keys
|
||||
.remove(&event.physical_key)
|
||||
{
|
||||
// Unlike key-presses, we can not tell whether a key-release event
|
||||
// is processed by the IME or not by looking at its `logical_key`,
|
||||
// because their `logical_key` is the original value (e.g.
|
||||
// `winit::keyboard::Key::Character(…)`) rather than
|
||||
// `winit::keyboard::Key::Named(winit::keyboard::NamedKey::Process)`.
|
||||
// (See the screencast for Windows in [^1].)
|
||||
// So we track the physical keys of processed key-presses and
|
||||
// filter out the corresponding key-releases.
|
||||
//
|
||||
// [^1]: https://github.com/rust-windowing/winit/issues/4508
|
||||
|
||||
Some(EventResponse {
|
||||
repaint: false,
|
||||
consumed: egui_wants_keyboard_input,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// ## NOTE
|
||||
///
|
||||
/// on Mac even Cmd-C is pressed during ime, a `c` is pushed to Preedit.
|
||||
@@ -548,23 +652,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 +714,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 +755,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`].
|
||||
@@ -974,6 +1105,16 @@ impl State {
|
||||
let allow_ime = ime.is_some();
|
||||
if self.allow_ime != allow_ime {
|
||||
self.allow_ime = allow_ime;
|
||||
#[cfg(target_os = "windows")]
|
||||
if !self.allow_ime {
|
||||
// Defensively clear the set to avoid unexpected behavior.
|
||||
//
|
||||
// We don't do the same in `ime_event_disable` because the key
|
||||
// release events for IME confirmation keys arrive after
|
||||
// `winit::event::Ime::Disabled`.
|
||||
self.pressed_processed_physical_keys.clear();
|
||||
}
|
||||
|
||||
profiling::scope!("set_ime_allowed");
|
||||
window.set_ime_allowed(allow_ime);
|
||||
}
|
||||
|
||||
@@ -36,8 +36,8 @@ mod ios {
|
||||
| UISceneActivationState::ForegroundInactive
|
||||
)
|
||||
{
|
||||
// Safe to cast, the class kind was checked above
|
||||
let window_scene = Retained::cast::<UIWindowScene>(scene.clone());
|
||||
// SAFETY: class kind was checked above with `isKindOfClass`
|
||||
let window_scene = Retained::cast_unchecked::<UIWindowScene>(scene.clone());
|
||||
if let Some(window) = window_scene.keyWindow() {
|
||||
let insets = window.safeAreaInsets();
|
||||
return SafeAreaInsets(MarginF32 {
|
||||
|
||||
@@ -11,7 +11,7 @@ readme = "../../README.md"
|
||||
repository = "https://github.com/emilk/egui"
|
||||
categories = ["gui", "game-development"]
|
||||
keywords = ["gui", "imgui", "immediate", "portable", "gamedev"]
|
||||
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
|
||||
include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
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.
|
||||
///
|
||||
/// Implements [`From`] for [`String`], [`str`], [`crate::Image`] and much more for convenience.
|
||||
/// This can be a piece of text, an image, or even a custom widget.
|
||||
/// It can be decorated with various layout hints, such as `grow`, `shrink`, `align`, and more.
|
||||
///
|
||||
/// `Atom` implements [`From`] for [`String`], [`str`], [`crate::Image`] and much more for convenience.
|
||||
///
|
||||
/// Many widgets take an `impl` [`crate::IntoAtoms`] parameter,
|
||||
/// which allows you to easily create atoms from tuples of text, images, and other atoms:
|
||||
/// ```
|
||||
/// # use egui::{Vec2, AtomExt, AtomKind, Atom, Image, Id};
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
/// let image = egui::include_image!("../../../eframe/data/icon.png");
|
||||
/// ui.button((image, "Click me!"));
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
/// You can directly call the `atom_*` methods on anything that implements `Into<Atom>`.
|
||||
/// ```
|
||||
/// # use egui::{Image, emath::Vec2};
|
||||
@@ -14,6 +28,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 +43,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 +76,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 +120,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,
|
||||
@@ -173,7 +203,7 @@ impl<'a> AtomLayout<'a> {
|
||||
// If the TextWrapMode is not Extend, ensure there is some item marked as `shrink`.
|
||||
// If none is found, mark the first text item as `shrink`.
|
||||
if wrap_mode != TextWrapMode::Extend {
|
||||
let any_shrink = atoms.iter().any(|a| a.shrink);
|
||||
let any_shrink = atoms.any_shrink();
|
||||
if !any_shrink {
|
||||
let first_text = atoms
|
||||
.iter_mut()
|
||||
@@ -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;
|
||||
|
||||
@@ -280,8 +318,9 @@ impl<'a> AtomLayout<'a> {
|
||||
let (_, rect) = ui.allocate_space(frame_size);
|
||||
let mut response = ui.interact(rect, id, sense);
|
||||
|
||||
response.intrinsic_size =
|
||||
Some((Vec2::new(intrinsic_width, intrinsic_height) + margin.sum()).at_least(min_size));
|
||||
response.set_intrinsic_size(
|
||||
(Vec2::new(intrinsic_width, intrinsic_height) + margin.sum()).at_least(min_size),
|
||||
);
|
||||
|
||||
AllocatedAtomLayout {
|
||||
sized_atoms: sized_items,
|
||||
@@ -321,7 +360,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 +370,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 +412,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 +464,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 +491,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 +511,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> {
|
||||
@@ -480,6 +521,20 @@ impl AtomLayoutResponse {
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for AtomLayoutResponse {
|
||||
type Target = Response;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.response
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for AtomLayoutResponse {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.response
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for AtomLayout<'_> {
|
||||
fn ui(self, ui: &mut Ui) -> Response {
|
||||
self.show(ui).response
|
||||
|
||||
@@ -8,6 +8,15 @@ use std::ops::{Deref, DerefMut};
|
||||
pub(crate) const ATOMS_SMALL_VEC_SIZE: usize = 2;
|
||||
|
||||
/// A list of [`Atom`]s.
|
||||
///
|
||||
/// Many widgets take an `impl` [`IntoAtoms`] parameter,
|
||||
/// which allows you to easily create atoms from tuples of text, images, and other atoms:
|
||||
/// ```
|
||||
/// # use egui::{AtomExt, AtomKind, Atom, Image, Id, Vec2};
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
/// let image = egui::include_image!("../../../eframe/data/icon.png");
|
||||
/// ui.button((image, "Click me!"));
|
||||
/// # });
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Atoms<'a>(SmallVec<[Atom<'a>; ATOMS_SMALL_VEC_SIZE]>);
|
||||
|
||||
@@ -21,11 +30,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.
|
||||
@@ -54,6 +78,11 @@ impl<'a> Atoms<'a> {
|
||||
string
|
||||
}
|
||||
|
||||
/// Do any of the atoms have shrink set to `true`?
|
||||
pub fn any_shrink(&self) -> bool {
|
||||
self.iter().any(|a| a.shrink)
|
||||
}
|
||||
|
||||
pub fn iter_kinds(&self) -> impl Iterator<Item = &AtomKind<'a>> {
|
||||
self.0.iter().map(|atom| &atom.kind)
|
||||
}
|
||||
@@ -172,6 +201,16 @@ where
|
||||
}
|
||||
|
||||
/// Trait for turning a tuple of [`Atom`]s into [`Atoms`].
|
||||
///
|
||||
/// Many widgets take an `impl` [`IntoAtoms`] parameter,
|
||||
/// which allows you to easily create atoms from tuples of text, images, and other atoms:
|
||||
/// ```
|
||||
/// # use egui::{AtomExt, AtomKind, Atom, Image, Id, Vec2};
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
/// let image = egui::include_image!("../../../eframe/data/icon.png");
|
||||
/// ui.button((image, "Click me!"));
|
||||
/// # });
|
||||
/// ```
|
||||
pub trait IntoAtoms<'a> {
|
||||
fn collect(self, atoms: &mut Atoms<'a>);
|
||||
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -516,6 +516,7 @@ impl Area {
|
||||
let move_response = ctx.create_widget(
|
||||
WidgetRect {
|
||||
id: interact_id,
|
||||
parent_id: id,
|
||||
layer_id,
|
||||
rect: state.rect(),
|
||||
interact_rect: state.rect().intersect(constrain_rect),
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
use crate::style::StyleModifier;
|
||||
use crate::{
|
||||
Button, Color32, Context, Frame, Id, InnerResponse, IntoAtoms, Layout, Popup,
|
||||
Button, Color32, Context, Frame, Id, InnerResponse, IntoAtoms, Layout, PointerButton, Popup,
|
||||
PopupCloseBehavior, Response, Style, Ui, UiBuilder, UiKind, UiStack, UiStackInfo, Widget as _,
|
||||
};
|
||||
use emath::{Align, RectAlign, Vec2, vec2};
|
||||
@@ -458,6 +458,7 @@ impl SubMenu {
|
||||
|
||||
let is_any_open = open_item.is_some();
|
||||
let mut is_open = open_item == Some(id);
|
||||
let was_open = is_open;
|
||||
let mut set_open = None;
|
||||
|
||||
// We expand the button rect so there is no empty space where no menu is shown
|
||||
@@ -470,9 +471,21 @@ impl SubMenu {
|
||||
// But since we check if no other menu is open, nothing should be able to cover the button
|
||||
let is_hovered = hover_pos.is_some_and(|pos| button_rect.contains(pos));
|
||||
|
||||
// `clicked` includes keyboard and accessibility click actions.
|
||||
// We want Enter/Space to toggle an already open submenu, while pointer clicks should keep
|
||||
// the submenu open (for touch and pointer interactions).
|
||||
let clicked = button_response.clicked();
|
||||
let clicked_by_pointer = button_response.clicked_by(PointerButton::Primary);
|
||||
let clicked_by_keyboard_or_access = clicked && !clicked_by_pointer;
|
||||
|
||||
if ui.is_enabled() && is_open && clicked_by_keyboard_or_access {
|
||||
set_open = Some(false);
|
||||
is_open = false;
|
||||
}
|
||||
|
||||
// The clicked handler is there for accessibility (keyboard navigation)
|
||||
let should_open =
|
||||
ui.is_enabled() && (button_response.clicked() || (is_hovered && !is_any_open));
|
||||
ui.is_enabled() && ((!was_open && clicked) || (is_hovered && !is_any_open));
|
||||
if should_open {
|
||||
set_open = Some(true);
|
||||
is_open = true;
|
||||
|
||||
@@ -561,7 +561,11 @@ impl Panel {
|
||||
let how_expanded = animate_expansion(ui.ctx(), self.id.with("animation"), is_expanded);
|
||||
|
||||
// Get either the fake or the real panel to animate
|
||||
let animated_panel = self.get_animated_panel(ui.ctx(), is_expanded)?;
|
||||
let Some(animated_panel) = self.get_animated_panel(ui.ctx(), is_expanded) else {
|
||||
// Make sure the ids of the next widgets are the same whether we show the panel or not:
|
||||
ui.skip_ahead_auto_ids(1);
|
||||
return None;
|
||||
};
|
||||
|
||||
if how_expanded < 1.0 {
|
||||
// Show a fake panel in this in-between animation state:
|
||||
|
||||
@@ -962,6 +962,7 @@ fn do_resize_interaction(
|
||||
WidgetRect {
|
||||
layer_id,
|
||||
id,
|
||||
parent_id: layer_id.id,
|
||||
rect,
|
||||
interact_rect: rect,
|
||||
sense: Sense::DRAG, // Don't use Sense::drag() since we don't want these to be focusable
|
||||
|
||||
@@ -1360,6 +1360,7 @@ impl Context {
|
||||
|
||||
let WidgetRect {
|
||||
id,
|
||||
parent_id: _,
|
||||
layer_id,
|
||||
rect,
|
||||
interact_rect,
|
||||
@@ -1378,8 +1379,8 @@ impl Context {
|
||||
interact_rect,
|
||||
sense,
|
||||
flags: Flags::empty(),
|
||||
interact_pointer_pos: None,
|
||||
intrinsic_size: None,
|
||||
interact_pointer_pos_or_nan: Pos2::NAN,
|
||||
intrinsic_size_or_nan: Vec2::NAN,
|
||||
};
|
||||
|
||||
res.flags.set(Flags::ENABLED, enabled);
|
||||
@@ -1470,14 +1471,11 @@ impl Context {
|
||||
|| res.long_touched()
|
||||
|| clicked
|
||||
|| res.drag_stopped();
|
||||
if is_interacted_with {
|
||||
res.interact_pointer_pos = input.pointer.interact_pos();
|
||||
if let (Some(to_global), Some(pos)) = (
|
||||
memory.to_global.get(&res.layer_id),
|
||||
&mut res.interact_pointer_pos,
|
||||
) {
|
||||
*pos = to_global.inverse() * *pos;
|
||||
if is_interacted_with && let Some(mut pos) = input.pointer.interact_pos() {
|
||||
if let Some(to_global) = memory.to_global.get(&res.layer_id) {
|
||||
pos = to_global.inverse() * pos;
|
||||
}
|
||||
res.interact_pointer_pos_or_nan = pos;
|
||||
}
|
||||
|
||||
if input.pointer.any_down() && !is_interacted_with {
|
||||
@@ -2397,6 +2395,12 @@ impl Context {
|
||||
crate::gui_zoom::zoom_with_keyboard(self);
|
||||
}
|
||||
|
||||
for shortcut in self.options(|o| o.quit_shortcuts.clone()) {
|
||||
if self.input_mut(|i| i.consume_shortcut(&shortcut)) {
|
||||
self.send_viewport_cmd(ViewportCommand::Close);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
self.debug_painting();
|
||||
|
||||
@@ -2616,6 +2620,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,
|
||||
});
|
||||
}
|
||||
@@ -2635,6 +2640,19 @@ impl ContextImpl {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
let shapes = if self.memory.options.style().debug.warn_if_rect_changes_id {
|
||||
let mut shapes = shapes;
|
||||
warn_if_rect_changes_id(
|
||||
&mut shapes,
|
||||
&viewport.prev_pass.widgets,
|
||||
&viewport.this_pass.widgets,
|
||||
);
|
||||
shapes
|
||||
} else {
|
||||
shapes
|
||||
};
|
||||
|
||||
std::mem::swap(&mut viewport.prev_pass, &mut viewport.this_pass);
|
||||
|
||||
if repaint_needed {
|
||||
@@ -3262,7 +3280,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;
|
||||
@@ -4236,6 +4254,112 @@ fn context_impl_send_sync() {
|
||||
assert_send_sync::<Context>();
|
||||
}
|
||||
|
||||
/// Check if any [`Rect`] appears with different [`Id`]s between two passes.
|
||||
///
|
||||
/// This helps detect cases where the same screen area is claimed by different widget ids
|
||||
/// across passes, which is often a sign of id instability.
|
||||
#[cfg(debug_assertions)]
|
||||
fn warn_if_rect_changes_id(
|
||||
out_shapes: &mut Vec<ClippedShape>,
|
||||
prev_widgets: &crate::WidgetRects,
|
||||
new_widgets: &crate::WidgetRects,
|
||||
) {
|
||||
profiling::function_scope!();
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// A wrapper around [`Rect`] that implements [`Ord`] using the bit representation of its floats.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
struct OrderedRect(Rect);
|
||||
|
||||
impl PartialOrd for OrderedRect {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for OrderedRect {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
let lhs = self.0;
|
||||
let rhs = other.0;
|
||||
lhs.min
|
||||
.x
|
||||
.to_bits()
|
||||
.cmp(&rhs.min.x.to_bits())
|
||||
.then(lhs.min.y.to_bits().cmp(&rhs.min.y.to_bits()))
|
||||
.then(lhs.max.x.to_bits().cmp(&rhs.max.x.to_bits()))
|
||||
.then(lhs.max.y.to_bits().cmp(&rhs.max.y.to_bits()))
|
||||
}
|
||||
}
|
||||
|
||||
fn create_lookup<'a>(
|
||||
widgets: impl Iterator<Item = &'a WidgetRect>,
|
||||
) -> BTreeMap<OrderedRect, Vec<&'a WidgetRect>> {
|
||||
let mut lookup: BTreeMap<OrderedRect, Vec<&'a WidgetRect>> = BTreeMap::default();
|
||||
for w in widgets {
|
||||
lookup.entry(OrderedRect(w.rect)).or_default().push(w);
|
||||
}
|
||||
lookup
|
||||
}
|
||||
|
||||
for (layer_id, new_layer_widgets) in new_widgets.layers() {
|
||||
let prev = create_lookup(prev_widgets.get_layer(*layer_id));
|
||||
let new = create_lookup(new_layer_widgets.iter());
|
||||
|
||||
for (hashable_rect, new_at_rect) in new {
|
||||
let Some(prev_at_rect) = prev.get(&hashable_rect) else {
|
||||
continue; // this rect did not exist in the previous pass
|
||||
};
|
||||
|
||||
if prev_at_rect
|
||||
.iter()
|
||||
.any(|w| new_at_rect.iter().any(|nw| nw.id == w.id))
|
||||
{
|
||||
continue; // at least one id stayed the same, so this is not an id change
|
||||
}
|
||||
|
||||
// Only warn if at least one of the previous ids is gone from this layer entirely.
|
||||
// If they all still exist (just at a different rect), then the rect match
|
||||
// is just a coincidence caused by widgets shifting (e.g. a window being dragged).
|
||||
if prev_at_rect.iter().all(|w| new_widgets.contains(w.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only warn if at least one widget has the same parent_id in both frames.
|
||||
// If all parent_ids changed too, this is a cascading id shift, not a widget bug.
|
||||
if !prev_at_rect
|
||||
.iter()
|
||||
.any(|pw| new_at_rect.iter().any(|nw| nw.parent_id == pw.parent_id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let rect = new_at_rect[0].rect;
|
||||
|
||||
log::warn!(
|
||||
"Widget rect {rect:?} changed id between passes: prev ids: {:?}, new ids: {:?}",
|
||||
prev_at_rect
|
||||
.iter()
|
||||
.map(|w| w.id.short_debug_format())
|
||||
.collect::<Vec<_>>(),
|
||||
new_at_rect
|
||||
.iter()
|
||||
.map(|w| w.id.short_debug_format())
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
out_shapes.push(ClippedShape {
|
||||
clip_rect: Rect::EVERYTHING,
|
||||
shape: epaint::Shape::rect_stroke(
|
||||
rect,
|
||||
0,
|
||||
(2.0, Color32::RED),
|
||||
StrokeKind::Outside,
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::Context;
|
||||
|
||||
@@ -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())
|
||||
@@ -410,6 +441,10 @@ pub enum Event {
|
||||
Text(String),
|
||||
|
||||
/// A key was pressed or released.
|
||||
///
|
||||
/// ## Note for integration authors
|
||||
///
|
||||
/// Key events that has been processed by IMEs should not be sent to `egui`.
|
||||
Key {
|
||||
/// Most of the time, it's the logical key, heeding the active keymap -- for instance, if the user has Dvorak
|
||||
/// keyboard layout, it will be taken into account.
|
||||
|
||||
@@ -450,6 +450,7 @@ mod tests {
|
||||
fn wr(id: Id, sense: Sense, rect: Rect) -> WidgetRect {
|
||||
WidgetRect {
|
||||
id,
|
||||
parent_id: Id::NULL,
|
||||
layer_id: LayerId::background(),
|
||||
rect,
|
||||
interact_rect: rect,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -623,12 +623,24 @@ impl Layout {
|
||||
if (self.is_vertical() && self.horizontal_align() == Align::Center)
|
||||
|| self.horizontal_justify()
|
||||
{
|
||||
frame_size.x = frame_size.x.max(available_rect.width()); // fill full width
|
||||
// For wrapping layouts, fill the current column width, not the entire layout width.
|
||||
let width = if self.main_wrap {
|
||||
region.cursor.width()
|
||||
} else {
|
||||
available_rect.width()
|
||||
};
|
||||
frame_size.x = frame_size.x.max(width); // fill full width
|
||||
}
|
||||
if (self.is_horizontal() && self.vertical_align() == Align::Center)
|
||||
|| self.vertical_justify()
|
||||
{
|
||||
frame_size.y = frame_size.y.max(available_rect.height()); // fill full height
|
||||
// For wrapping layouts, fill the current row height, not the entire layout height.
|
||||
let height = if self.main_wrap {
|
||||
region.cursor.height()
|
||||
} else {
|
||||
available_rect.height()
|
||||
};
|
||||
frame_size.y = frame_size.y.max(height); // fill full height
|
||||
}
|
||||
|
||||
let align2 = match self.main_dir {
|
||||
@@ -791,14 +803,14 @@ impl Layout {
|
||||
let new_top = region.cursor.bottom() + spacing.y;
|
||||
region.cursor = Rect::from_min_max(
|
||||
pos2(region.max_rect.left(), new_top),
|
||||
pos2(INFINITY, new_top + region.cursor.height()),
|
||||
pos2(INFINITY, new_top),
|
||||
);
|
||||
}
|
||||
Direction::RightToLeft => {
|
||||
let new_top = region.cursor.bottom() + spacing.y;
|
||||
region.cursor = Rect::from_min_max(
|
||||
pos2(-INFINITY, new_top),
|
||||
pos2(region.max_rect.right(), new_top + region.cursor.height()),
|
||||
pos2(region.max_rect.right(), new_top),
|
||||
);
|
||||
}
|
||||
Direction::TopDown | Direction::BottomUp => {}
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -234,6 +234,16 @@ pub struct Options {
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
pub zoom_with_keyboard: bool,
|
||||
|
||||
/// Keyboard shortcuts to close the application.
|
||||
///
|
||||
/// Pressing any of these will send [`crate::ViewportCommand::Close`]
|
||||
/// to the root viewport.
|
||||
///
|
||||
/// Defaults to `Cmd-Q` (which is Ctrl-Q on Linux/Windows, Cmd-Q on Mac).
|
||||
/// Set to empty to disable.
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
pub quit_shortcuts: Vec<crate::KeyboardShortcut>,
|
||||
|
||||
/// Controls the tessellator.
|
||||
pub tessellation_options: epaint::TessellationOptions,
|
||||
|
||||
@@ -304,6 +314,10 @@ impl Default for Options {
|
||||
system_theme: None,
|
||||
zoom_factor: 1.0,
|
||||
zoom_with_keyboard: true,
|
||||
quit_shortcuts: vec![crate::KeyboardShortcut::new(
|
||||
crate::Modifiers::COMMAND,
|
||||
crate::Key::Q,
|
||||
)],
|
||||
tessellation_options: Default::default(),
|
||||
repaint_on_widget_change: false,
|
||||
|
||||
@@ -363,6 +377,7 @@ impl Options {
|
||||
system_theme: _,
|
||||
zoom_factor,
|
||||
zoom_with_keyboard,
|
||||
quit_shortcuts: _, // not shown in ui
|
||||
tessellation_options,
|
||||
repaint_on_widget_change,
|
||||
max_passes,
|
||||
@@ -564,11 +579,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ pub struct Response {
|
||||
/// Where the pointer (mouse/touch) were when this widget was clicked or dragged.
|
||||
/// `None` if the widget is not being interacted with.
|
||||
#[doc(hidden)]
|
||||
pub interact_pointer_pos: Option<Pos2>,
|
||||
pub interact_pointer_pos_or_nan: Pos2,
|
||||
|
||||
/// The intrinsic / desired size of the widget.
|
||||
///
|
||||
@@ -67,12 +67,22 @@ pub struct Response {
|
||||
/// At the time of writing, this is only used by external crates
|
||||
/// for improved layouting.
|
||||
/// See for instance [`egui_flex`](https://github.com/lucasmerlin/hello_egui/tree/main/crates/egui_flex).
|
||||
pub intrinsic_size: Option<Vec2>,
|
||||
#[doc(hidden)]
|
||||
pub intrinsic_size_or_nan: Vec2,
|
||||
|
||||
#[doc(hidden)]
|
||||
pub flags: Flags,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_size() {
|
||||
assert_eq!(
|
||||
std::mem::size_of::<Response>(),
|
||||
88,
|
||||
"Keep Response small, because we create them often, and we want to keep it lean and fast"
|
||||
);
|
||||
}
|
||||
|
||||
/// A bit set for various boolean properties of `Response`.
|
||||
#[doc(hidden)]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
@@ -141,6 +151,22 @@ bitflags::bitflags! {
|
||||
}
|
||||
|
||||
impl Response {
|
||||
/// The [`Id`] of the parent [`crate::Ui`] that hosts this widget.
|
||||
///
|
||||
/// Looks up the [`WidgetRect`] from the current (or previous) pass.
|
||||
pub fn parent_id(&self) -> Id {
|
||||
let id = self.ctx.viewport(|viewport| {
|
||||
viewport
|
||||
.this_pass
|
||||
.widgets
|
||||
.get(self.id)
|
||||
.or_else(|| viewport.prev_pass.widgets.get(self.id))
|
||||
.map(|w| w.parent_id)
|
||||
});
|
||||
debug_assert!(id.is_some(), "WidgetRect for Response not found!");
|
||||
id.unwrap_or(Id::NULL)
|
||||
}
|
||||
|
||||
/// Returns true if this widget was clicked this frame by the primary button.
|
||||
///
|
||||
/// A click is registered when the mouse or touch is released within
|
||||
@@ -489,7 +515,26 @@ impl Response {
|
||||
/// `None` if the widget is not being interacted with.
|
||||
#[inline]
|
||||
pub fn interact_pointer_pos(&self) -> Option<Pos2> {
|
||||
self.interact_pointer_pos
|
||||
let pos = self.interact_pointer_pos_or_nan;
|
||||
if pos.any_nan() { None } else { Some(pos) }
|
||||
}
|
||||
|
||||
/// The intrinsic / desired size of the widget.
|
||||
///
|
||||
/// This is the size that a non-wrapped, non-truncated, non-justified version of the widget
|
||||
/// would have.
|
||||
///
|
||||
/// If this is `None`, use [`Self::rect`] instead.
|
||||
#[inline]
|
||||
pub fn intrinsic_size(&self) -> Option<Vec2> {
|
||||
let size = self.intrinsic_size_or_nan;
|
||||
if size.any_nan() { None } else { Some(size) }
|
||||
}
|
||||
|
||||
/// Set the intrinsic / desired size of the widget.
|
||||
#[inline]
|
||||
pub fn set_intrinsic_size(&mut self, size: Vec2) {
|
||||
self.intrinsic_size_or_nan = size;
|
||||
}
|
||||
|
||||
/// If it is a good idea to show a tooltip, where is pointer?
|
||||
@@ -732,6 +777,7 @@ impl Response {
|
||||
WidgetRect {
|
||||
layer_id: self.layer_id,
|
||||
id: self.id,
|
||||
parent_id: self.parent_id(),
|
||||
rect: self.rect,
|
||||
interact_rect: self.interact_rect,
|
||||
sense: self.sense | sense,
|
||||
@@ -1007,8 +1053,10 @@ impl Response {
|
||||
interact_rect: self.interact_rect.union(other.interact_rect),
|
||||
sense: self.sense.union(other.sense),
|
||||
flags: self.flags | other.flags,
|
||||
interact_pointer_pos: self.interact_pointer_pos.or(other.interact_pointer_pos),
|
||||
intrinsic_size: None,
|
||||
interact_pointer_pos_or_nan: self
|
||||
.interact_pointer_pos()
|
||||
.unwrap_or(other.interact_pointer_pos_or_nan),
|
||||
intrinsic_size_or_nan: Vec2::NAN,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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::{
|
||||
@@ -1319,6 +1323,10 @@ pub struct DebugOptions {
|
||||
/// Show interesting widgets under the mouse cursor.
|
||||
pub show_widget_hits: bool,
|
||||
|
||||
/// Show a warning if the same `Rect` had different `Id` and the same parent `Id` on the
|
||||
/// previous frame.
|
||||
pub warn_if_rect_changes_id: bool,
|
||||
|
||||
/// If true, highlight widgets that are not aligned to [`emath::GUI_ROUNDING`].
|
||||
///
|
||||
/// See [`emath::GuiRounding`] for more.
|
||||
@@ -1345,6 +1353,7 @@ impl Default for DebugOptions {
|
||||
show_resize: false,
|
||||
show_interactive_widgets: false,
|
||||
show_widget_hits: false,
|
||||
warn_if_rect_changes_id: cfg!(debug_assertions),
|
||||
show_unaligned: cfg!(debug_assertions),
|
||||
show_focused_widget: false,
|
||||
}
|
||||
@@ -2526,6 +2535,7 @@ impl DebugOptions {
|
||||
show_resize,
|
||||
show_interactive_widgets,
|
||||
show_widget_hits,
|
||||
warn_if_rect_changes_id,
|
||||
show_unaligned,
|
||||
show_focused_widget,
|
||||
} = self;
|
||||
@@ -2557,6 +2567,11 @@ impl DebugOptions {
|
||||
|
||||
ui.checkbox(show_widget_hits, "Show widgets under mouse pointer");
|
||||
|
||||
ui.checkbox(
|
||||
warn_if_rect_changes_id,
|
||||
"Warn if a Rect changes Id between frames",
|
||||
);
|
||||
|
||||
ui.checkbox(
|
||||
show_unaligned,
|
||||
"Show rectangles not aligned to integer point coordinates",
|
||||
@@ -2876,7 +2891,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)
|
||||
@@ -2886,6 +2901,7 @@ impl Widget for &mut FontTweak {
|
||||
y_offset_factor,
|
||||
y_offset,
|
||||
hinting_override,
|
||||
coords,
|
||||
} = self;
|
||||
|
||||
ui.label("Scale");
|
||||
@@ -2913,6 +2929,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)
|
||||
|
||||
@@ -173,6 +173,7 @@ impl Ui {
|
||||
ui.ctx().create_widget(
|
||||
WidgetRect {
|
||||
id: ui.unique_id,
|
||||
parent_id: ui.id,
|
||||
layer_id: ui.layer_id(),
|
||||
rect: start_rect,
|
||||
interact_rect: start_rect,
|
||||
@@ -339,6 +340,7 @@ impl Ui {
|
||||
child_ui.ctx().create_widget(
|
||||
WidgetRect {
|
||||
id: child_ui.unique_id,
|
||||
parent_id: self.id,
|
||||
layer_id: child_ui.layer_id(),
|
||||
rect: start_rect,
|
||||
interact_rect: start_rect,
|
||||
@@ -1043,6 +1045,7 @@ impl Ui {
|
||||
self.ctx().create_widget(
|
||||
WidgetRect {
|
||||
id,
|
||||
parent_id: self.id,
|
||||
layer_id: self.layer_id(),
|
||||
rect,
|
||||
interact_rect: self.clip_rect().intersect(rect),
|
||||
@@ -1112,6 +1115,7 @@ impl Ui {
|
||||
let mut response = self.ctx().create_widget(
|
||||
WidgetRect {
|
||||
id: self.unique_id,
|
||||
parent_id: self.id,
|
||||
layer_id: self.layer_id(),
|
||||
rect: self.min_rect(),
|
||||
interact_rect: self.clip_rect().intersect(self.min_rect()),
|
||||
@@ -1281,7 +1285,7 @@ impl Ui {
|
||||
pub fn allocate_response(&mut self, desired_size: Vec2, sense: Sense) -> Response {
|
||||
let (id, rect) = self.allocate_space(desired_size);
|
||||
let mut response = self.interact(rect, id, sense);
|
||||
response.intrinsic_size = Some(desired_size);
|
||||
response.set_intrinsic_size(desired_size);
|
||||
response
|
||||
}
|
||||
|
||||
|
||||
@@ -54,8 +54,8 @@ impl UiBuilder {
|
||||
///
|
||||
/// This is a shortcut for `.id_salt(my_id).global_scope(true)`.
|
||||
#[inline]
|
||||
pub fn id(mut self, id: impl Hash) -> Self {
|
||||
self.id_salt = Some(Id::new(id));
|
||||
pub fn id(mut self, id: Id) -> Self {
|
||||
self.id_salt = Some(id);
|
||||
self.global_scope = true;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -17,6 +17,12 @@ pub struct WidgetRect {
|
||||
/// You can ensure globally unique ids using [`crate::Ui::push_id`].
|
||||
pub id: Id,
|
||||
|
||||
/// The [`Id`] of the parent [`crate::Ui`] that hosts this widget.
|
||||
///
|
||||
/// Used by debug checks to distinguish true id-instability from
|
||||
/// cascading id shifts caused by a parent Ui's auto-id changing.
|
||||
pub parent_id: Id,
|
||||
|
||||
/// What layer the widget is on.
|
||||
pub layer_id: LayerId,
|
||||
|
||||
@@ -46,6 +52,7 @@ impl WidgetRect {
|
||||
pub fn transform(self, transform: emath::TSTransform) -> Self {
|
||||
let Self {
|
||||
id,
|
||||
parent_id,
|
||||
layer_id,
|
||||
rect,
|
||||
interact_rect,
|
||||
@@ -54,6 +61,7 @@ impl WidgetRect {
|
||||
} = self;
|
||||
Self {
|
||||
id,
|
||||
parent_id,
|
||||
layer_id,
|
||||
rect: transform * rect,
|
||||
interact_rect: transform * interact_rect,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -240,6 +240,18 @@ impl<'a> Button<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Show some text on the left side of the button.
|
||||
#[inline]
|
||||
pub fn left_text(mut self, left_text: impl IntoAtoms<'a>) -> Self {
|
||||
self.layout.push_left(Atom::grow());
|
||||
|
||||
for atom in left_text.into_atoms() {
|
||||
self.layout.push_left(atom);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Show some text on the right side of the button.
|
||||
#[inline]
|
||||
pub fn right_text(mut self, right_text: impl IntoAtoms<'a>) -> Self {
|
||||
@@ -259,6 +271,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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -220,7 +220,7 @@ impl Label {
|
||||
.rect_without_leading_space()
|
||||
.translate(pos.to_vec2());
|
||||
let mut response = ui.allocate_rect(rect, sense);
|
||||
response.intrinsic_size = Some(galley.intrinsic_size());
|
||||
response.set_intrinsic_size(galley.intrinsic_size());
|
||||
for placed_row in galley.rows.iter().skip(1) {
|
||||
let rect = placed_row.rect().translate(pos.to_vec2());
|
||||
response |= ui.allocate_rect(rect, sense);
|
||||
@@ -256,7 +256,7 @@ impl Label {
|
||||
|
||||
let galley = ui.fonts_mut(|fonts| fonts.layout_job(layout_job));
|
||||
let (rect, mut response) = ui.allocate_exact_size(galley.size(), sense);
|
||||
response.intrinsic_size = Some(galley.intrinsic_size());
|
||||
response.set_intrinsic_size(galley.intrinsic_size());
|
||||
let galley_pos = match galley.job.halign {
|
||||
Align::LEFT => rect.left_top(),
|
||||
Align::Center => rect.center_top(),
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use emath::{Rect, TSTransform};
|
||||
use epaint::{
|
||||
StrokeKind,
|
||||
text::{Galley, LayoutJob, cursor::CCursor},
|
||||
};
|
||||
use epaint::text::{Galley, LayoutJob, TextWrapMode, cursor::CCursor};
|
||||
|
||||
use crate::{
|
||||
Align, Align2, Color32, Context, CursorIcon, Event, EventFilter, FontSelection, Id, ImeEvent,
|
||||
Key, KeyboardShortcut, Margin, Modifiers, NumExt as _, Response, Sense, Shape, TextBuffer,
|
||||
TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetWithState, epaint,
|
||||
Align, Align2, Atom, AtomExt as _, AtomKind, AtomLayout, Atoms, Color32, Context, CursorIcon,
|
||||
Event, EventFilter, FontSelection, Frame, Id, ImeEvent, IntoAtoms, IntoSizedResult, Key,
|
||||
KeyboardShortcut, Margin, Modifiers, NumExt as _, Response, Sense, SizedAtomKind, TextBuffer,
|
||||
TextStyle, Ui, Vec2, Widget, WidgetInfo, WidgetWithState, epaint,
|
||||
os::OperatingSystem,
|
||||
output::OutputEvent,
|
||||
response, text_selection,
|
||||
@@ -67,15 +65,16 @@ type LayouterFn<'t> = &'t mut dyn FnMut(&Ui, &dyn TextBuffer, f32) -> Arc<Galley
|
||||
#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
|
||||
pub struct TextEdit<'t> {
|
||||
text: &'t mut dyn TextBuffer,
|
||||
hint_text: WidgetText,
|
||||
hint_text_font: Option<FontSelection>,
|
||||
prefix: Atoms<'static>,
|
||||
suffix: Atoms<'static>,
|
||||
hint_text: Atoms<'static>,
|
||||
id: Option<Id>,
|
||||
id_salt: Option<Id>,
|
||||
font_selection: FontSelection,
|
||||
text_color: Option<Color32>,
|
||||
layouter: Option<LayouterFn<'t>>,
|
||||
password: bool,
|
||||
frame: bool,
|
||||
frame: Option<Frame>,
|
||||
margin: Margin,
|
||||
multiline: bool,
|
||||
interactive: bool,
|
||||
@@ -120,15 +119,16 @@ impl<'t> TextEdit<'t> {
|
||||
pub fn multiline(text: &'t mut dyn TextBuffer) -> Self {
|
||||
Self {
|
||||
text,
|
||||
prefix: Default::default(),
|
||||
suffix: Default::default(),
|
||||
hint_text: Default::default(),
|
||||
hint_text_font: None,
|
||||
id: None,
|
||||
id_salt: None,
|
||||
font_selection: Default::default(),
|
||||
text_color: None,
|
||||
layouter: None,
|
||||
password: false,
|
||||
frame: true,
|
||||
frame: None,
|
||||
margin: Margin::symmetric(4, 2),
|
||||
multiline: true,
|
||||
interactive: true,
|
||||
@@ -202,8 +202,22 @@ impl<'t> TextEdit<'t> {
|
||||
/// # });
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn hint_text(mut self, hint_text: impl Into<WidgetText>) -> Self {
|
||||
self.hint_text = hint_text.into();
|
||||
pub fn hint_text(mut self, hint_text: impl IntoAtoms<'static>) -> Self {
|
||||
self.hint_text = hint_text.into_atoms();
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a prefix to the text edit. This will always be shown before the editable text.
|
||||
#[inline]
|
||||
pub fn prefix(mut self, prefix: impl IntoAtoms<'static>) -> Self {
|
||||
self.prefix = prefix.into_atoms();
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a suffix to the text edit. This will always be shown after the editable text.
|
||||
#[inline]
|
||||
pub fn suffix(mut self, suffix: impl IntoAtoms<'static>) -> Self {
|
||||
self.suffix = suffix.into_atoms();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -215,13 +229,6 @@ impl<'t> TextEdit<'t> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set a specific style for the hint text.
|
||||
#[inline]
|
||||
pub fn hint_text_font(mut self, hint_text_font: impl Into<FontSelection>) -> Self {
|
||||
self.hint_text_font = Some(hint_text_font.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// If true, hide the letters from view and prevent copying from the field.
|
||||
#[inline]
|
||||
pub fn password(mut self, password: bool) -> Self {
|
||||
@@ -290,10 +297,10 @@ impl<'t> TextEdit<'t> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Default is `true`. If set to `false` there will be no frame showing that this is editable text!
|
||||
/// Customize the [`Frame`] around the text edit.
|
||||
#[inline]
|
||||
pub fn frame(mut self, frame: bool) -> Self {
|
||||
self.frame = frame;
|
||||
pub fn frame(mut self, frame: Frame) -> Self {
|
||||
self.frame = Some(frame);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -402,7 +409,7 @@ impl<'t> TextEdit<'t> {
|
||||
|
||||
impl Widget for TextEdit<'_> {
|
||||
fn ui(self, ui: &mut Ui) -> Response {
|
||||
self.show(ui).response
|
||||
self.show(ui).response.response
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,63 +430,18 @@ impl TextEdit<'_> {
|
||||
/// # });
|
||||
/// ```
|
||||
pub fn show(self, ui: &mut Ui) -> TextEditOutput {
|
||||
let is_mutable = self.text.is_mutable();
|
||||
let frame = self.frame;
|
||||
let where_to_put_background = ui.painter().add(Shape::Noop);
|
||||
let background_color = self
|
||||
.background_color
|
||||
.unwrap_or_else(|| ui.visuals().text_edit_bg_color());
|
||||
let output = self.show_content(ui);
|
||||
|
||||
if frame {
|
||||
let visuals = ui.style().interact(&output.response);
|
||||
let frame_rect = output.response.rect.expand(visuals.expansion);
|
||||
let shape = if is_mutable {
|
||||
if output.response.has_focus() {
|
||||
epaint::RectShape::new(
|
||||
frame_rect,
|
||||
visuals.corner_radius,
|
||||
background_color,
|
||||
ui.visuals().selection.stroke,
|
||||
StrokeKind::Inside,
|
||||
)
|
||||
} else {
|
||||
epaint::RectShape::new(
|
||||
frame_rect,
|
||||
visuals.corner_radius,
|
||||
background_color,
|
||||
visuals.bg_stroke, // TODO(emilk): we want to show something here, or a text-edit field doesn't "pop".
|
||||
StrokeKind::Inside,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
let visuals = &ui.style().visuals.widgets.inactive;
|
||||
epaint::RectShape::stroke(
|
||||
frame_rect,
|
||||
visuals.corner_radius,
|
||||
visuals.bg_stroke, // TODO(emilk): we want to show something here, or a text-edit field doesn't "pop".
|
||||
StrokeKind::Inside,
|
||||
)
|
||||
};
|
||||
|
||||
ui.painter().set(where_to_put_background, shape);
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
fn show_content(self, ui: &mut Ui) -> TextEditOutput {
|
||||
let TextEdit {
|
||||
text,
|
||||
hint_text,
|
||||
hint_text_font,
|
||||
prefix,
|
||||
suffix,
|
||||
mut hint_text,
|
||||
id,
|
||||
id_salt,
|
||||
font_selection,
|
||||
text_color,
|
||||
layouter,
|
||||
password,
|
||||
frame: _,
|
||||
frame,
|
||||
margin,
|
||||
multiline,
|
||||
interactive,
|
||||
@@ -492,7 +454,7 @@ impl TextEdit<'_> {
|
||||
clip_text,
|
||||
char_limit,
|
||||
return_key,
|
||||
background_color: _,
|
||||
background_color,
|
||||
} = self;
|
||||
|
||||
let text_color = text_color
|
||||
@@ -501,18 +463,16 @@ impl TextEdit<'_> {
|
||||
.unwrap_or_else(|| ui.visuals().widgets.inactive.text_color());
|
||||
|
||||
let prev_text = text.as_str().to_owned();
|
||||
let hint_text_str = hint_text.text().to_owned();
|
||||
let hint_text_str = hint_text.text().unwrap_or_default().to_string();
|
||||
|
||||
let font_id = font_selection.resolve(ui.style());
|
||||
let row_height = ui.fonts_mut(|f| f.row_height(&font_id));
|
||||
const MIN_WIDTH: f32 = 24.0; // Never make a [`TextEdit`] more narrow than this.
|
||||
let available_width = (ui.available_width() - margin.sum().x).at_least(MIN_WIDTH);
|
||||
let desired_width = desired_width.unwrap_or_else(|| ui.spacing().text_edit_width);
|
||||
let wrap_width = if ui.layout().horizontal_justify() {
|
||||
available_width
|
||||
} else {
|
||||
desired_width.min(available_width)
|
||||
};
|
||||
let available_width = ui.available_width().at_least(MIN_WIDTH);
|
||||
let desired_width = desired_width
|
||||
.unwrap_or_else(|| ui.spacing().text_edit_width)
|
||||
.at_least(min_size.x);
|
||||
let allocate_width = desired_width.at_most(available_width);
|
||||
|
||||
let font_id_clone = font_id.clone();
|
||||
let mut default_layouter = move |ui: &Ui, text: &dyn TextBuffer, wrap_width: f32| {
|
||||
@@ -527,27 +487,18 @@ impl TextEdit<'_> {
|
||||
|
||||
let layouter = layouter.unwrap_or(&mut default_layouter);
|
||||
|
||||
let mut galley = layouter(ui, text, wrap_width);
|
||||
|
||||
let desired_inner_width = if clip_text {
|
||||
wrap_width // visual clipping with scroll in singleline input.
|
||||
} else {
|
||||
galley.size().x.max(wrap_width)
|
||||
};
|
||||
let desired_height = (desired_height_rows.at_least(1) as f32) * row_height;
|
||||
let desired_inner_size = vec2(desired_inner_width, galley.size().y.max(desired_height));
|
||||
let desired_outer_size = (desired_inner_size + margin.sum()).at_least(min_size);
|
||||
let (auto_id, outer_rect) = ui.allocate_space(desired_outer_size);
|
||||
let rect = outer_rect - margin; // inner rect (excluding frame/margin).
|
||||
let min_inner_height = (desired_height_rows.at_least(1) as f32) * row_height;
|
||||
|
||||
let id = id.unwrap_or_else(|| {
|
||||
if let Some(id_salt) = id_salt {
|
||||
ui.make_persistent_id(id_salt)
|
||||
} else {
|
||||
auto_id // Since we are only storing the cursor a persistent Id is not super important
|
||||
// Since we are only storing the cursor a persistent Id is not super important
|
||||
let id = ui.next_auto_id();
|
||||
ui.skip_ahead_auto_ids(1);
|
||||
id
|
||||
}
|
||||
});
|
||||
let mut state = TextEditState::load(ui.ctx(), id).unwrap_or_default();
|
||||
|
||||
// On touch screens (e.g. mobile in `eframe` web), should
|
||||
// dragging select text, or scroll the enclosing [`ScrollArea`] (if any)?
|
||||
@@ -565,12 +516,218 @@ impl TextEdit<'_> {
|
||||
} else {
|
||||
Sense::hover()
|
||||
};
|
||||
let mut response = ui.interact(outer_rect, id, sense);
|
||||
response.intrinsic_size = Some(Vec2::new(desired_width, desired_outer_size.y));
|
||||
|
||||
// Don't sent `OutputEvent::Clicked` when a user presses the space bar
|
||||
let mut state = TextEditState::load(ui.ctx(), id).unwrap_or_default();
|
||||
let mut cursor_range = None;
|
||||
let mut prev_cursor_range = None;
|
||||
|
||||
let mut text_changed = false;
|
||||
let text_mutable = text.is_mutable();
|
||||
|
||||
let mut handle_events = |ui: &Ui, galley: &mut Arc<Galley>, layouter, wrap_width, text| {
|
||||
if interactive && ui.memory(|mem| mem.has_focus(id)) {
|
||||
ui.memory_mut(|mem| mem.set_focus_lock_filter(id, event_filter));
|
||||
|
||||
let default_cursor_range = if cursor_at_end {
|
||||
CCursorRange::one(galley.end())
|
||||
} else {
|
||||
CCursorRange::default()
|
||||
};
|
||||
prev_cursor_range = state.cursor.range(galley);
|
||||
|
||||
let (changed, new_cursor_range) = events(
|
||||
ui,
|
||||
&mut state,
|
||||
text,
|
||||
galley,
|
||||
layouter,
|
||||
id,
|
||||
wrap_width,
|
||||
multiline,
|
||||
password,
|
||||
default_cursor_range,
|
||||
char_limit,
|
||||
event_filter,
|
||||
return_key,
|
||||
);
|
||||
|
||||
if changed {
|
||||
text_changed = true;
|
||||
}
|
||||
cursor_range = Some(new_cursor_range);
|
||||
}
|
||||
};
|
||||
|
||||
// We need to calculate the galley within the atom closure, so we can calculate it based on
|
||||
// the available width (in case of wrapping multiline text edits). But we show it later,
|
||||
// so we can clip it to the available size. Thus, extract it from the atom closure here.
|
||||
let mut get_galley = None;
|
||||
let inner_rect_id = Id::new("text_edit_rect");
|
||||
let mut response = {
|
||||
let any_shrink = hint_text.any_shrink();
|
||||
// Ideally we could just do `let mut atoms = prefix` here, but that won't compile
|
||||
// but due to servo/rust-smallvec#146 (also see the comment below).
|
||||
let mut atoms: Atoms<'_> = Atoms::new(());
|
||||
|
||||
// TODO(servo/rust-smallvec#146): Use extend_right instead of the loop once we have
|
||||
// smallvec 2.0. Using `extend_right` here won't compile, due to lifetime issues.
|
||||
for atom in prefix {
|
||||
atoms.push_right(atom);
|
||||
}
|
||||
|
||||
if text.as_str().is_empty() && !hint_text.is_empty() {
|
||||
// Add hint_text (if any):
|
||||
let mut shrunk = any_shrink;
|
||||
let mut first = true;
|
||||
|
||||
// Since we can't set a fallback color per atom, we have to override it here.
|
||||
// Sucks, since it means users won't be able to override it.
|
||||
hint_text.map_texts(|t| t.color(ui.style().visuals.weak_text_color()));
|
||||
|
||||
for mut atom in hint_text {
|
||||
if !shrunk && matches!(atom.kind, AtomKind::Text(_)) {
|
||||
// elide the hint_text if needed
|
||||
atom = atom.atom_shrink(true);
|
||||
shrunk = true;
|
||||
}
|
||||
|
||||
if first {
|
||||
// The first atom in the hint text gets inner_rect_id, so we can know
|
||||
// where to paint the cursor
|
||||
atom = atom.atom_id(inner_rect_id);
|
||||
first = false;
|
||||
}
|
||||
|
||||
// The hint text should be shown left top instead of centered (important for
|
||||
// multi line text edits)
|
||||
atoms.push_right(atom.atom_align(Align2::LEFT_TOP));
|
||||
}
|
||||
|
||||
// Calculate the empty galley, so it can be read later. The available width is
|
||||
// technically wrong, but doesn't matter since the galley is empty
|
||||
let available_width = allocate_width - margin.sum().x;
|
||||
let galley = layouter(ui, text, available_width);
|
||||
|
||||
// We can't update the galley immediately here, since it would show both hint text
|
||||
// and the newly typed letter. So we pass a clone instead, and accept having a frame
|
||||
// delay on the very first keystroke.
|
||||
let mut galley_clone = Arc::clone(&galley);
|
||||
handle_events(ui, &mut galley_clone, layouter, available_width, text);
|
||||
|
||||
get_galley = Some(galley);
|
||||
} else {
|
||||
// We need a closure here, so we can calculate the galley based on the available
|
||||
// width (after adding suffix and prefix), for correct wrapping in multi line text
|
||||
// edits
|
||||
atoms.push_right(
|
||||
AtomKind::closure(|ui, args| {
|
||||
let mut galley = layouter(ui, text, args.available_size.x);
|
||||
|
||||
// Handling events here allows us to update the galley immediately on
|
||||
// keystrokes, avoiding frame delays, and ensuring the scroll_to within
|
||||
// ScrollAreas works correctly.
|
||||
handle_events(ui, &mut galley, layouter, args.available_size.x, text);
|
||||
|
||||
let intrinsic_size = galley.intrinsic_size();
|
||||
let mut size = galley.size();
|
||||
size.y = size.y.at_least(min_inner_height);
|
||||
if clip_text {
|
||||
size.x = size.x.at_most(args.available_size.x);
|
||||
}
|
||||
|
||||
// We paint the galley later, so we can do clipping and offsetting
|
||||
get_galley = Some(galley);
|
||||
IntoSizedResult {
|
||||
intrinsic_size,
|
||||
sized: SizedAtomKind::Empty { size: Some(size) },
|
||||
}
|
||||
})
|
||||
.atom_id(inner_rect_id)
|
||||
.atom_shrink(clip_text),
|
||||
);
|
||||
}
|
||||
|
||||
// Ensure the suffix is always right-aligned
|
||||
if !suffix.is_empty() {
|
||||
atoms.push_right(Atom::grow());
|
||||
}
|
||||
|
||||
// TODO(servo/rust-smallvec#146): Use extend_right instead of the loop once we have
|
||||
// smallvec 2.0. Using `extend_right` here won't compile, due to lifetime issues.
|
||||
for atom in suffix {
|
||||
atoms.push_right(atom);
|
||||
}
|
||||
|
||||
let custom_frame = frame.is_some();
|
||||
let frame = frame.unwrap_or_else(|| Frame::new().inner_margin(margin));
|
||||
|
||||
let min_height = min_inner_height + frame.total_margin().sum().y;
|
||||
|
||||
// This wrap mode only affects the hint_text
|
||||
let wrap_mode = if multiline {
|
||||
TextWrapMode::Wrap
|
||||
} else {
|
||||
TextWrapMode::Truncate
|
||||
};
|
||||
|
||||
let mut allocated = AtomLayout::new(atoms)
|
||||
.id(id)
|
||||
.min_size(Vec2::new(allocate_width, min_height))
|
||||
.max_width(allocate_width)
|
||||
.sense(sense)
|
||||
.frame(frame)
|
||||
.align2(Align2::LEFT_TOP)
|
||||
.wrap_mode(wrap_mode)
|
||||
.allocate(ui);
|
||||
|
||||
allocated.frame = if !custom_frame {
|
||||
let visuals = ui.style().interact(&allocated.response);
|
||||
let background_color =
|
||||
background_color.unwrap_or_else(|| ui.visuals().text_edit_bg_color());
|
||||
|
||||
let (corner_radius, background_color, stroke) = if text_mutable {
|
||||
if allocated.response.has_focus() {
|
||||
(
|
||||
visuals.corner_radius,
|
||||
background_color,
|
||||
ui.visuals().selection.stroke,
|
||||
)
|
||||
} else {
|
||||
(visuals.corner_radius, background_color, visuals.bg_stroke)
|
||||
}
|
||||
} else {
|
||||
let visuals = &ui.style().visuals.widgets.inactive;
|
||||
(
|
||||
visuals.corner_radius,
|
||||
Color32::TRANSPARENT,
|
||||
visuals.bg_stroke,
|
||||
)
|
||||
};
|
||||
allocated
|
||||
.frame
|
||||
.fill(background_color)
|
||||
.corner_radius(corner_radius)
|
||||
.inner_margin(
|
||||
allocated.frame.inner_margin
|
||||
+ Margin::same((visuals.expansion - stroke.width).round() as i8),
|
||||
)
|
||||
.outer_margin(Margin::same(-(visuals.expansion as i8)))
|
||||
.stroke(stroke)
|
||||
} else {
|
||||
allocated.frame
|
||||
};
|
||||
|
||||
allocated.paint(ui)
|
||||
};
|
||||
|
||||
let inner_rect = response.rect(inner_rect_id).unwrap_or(Rect::ZERO);
|
||||
|
||||
// Our atom closure was now called, so the galley should always be available here
|
||||
let mut galley = get_galley.expect("Galley should be available here");
|
||||
|
||||
// Don't send `OutputEvent::Clicked` when a user presses the space bar
|
||||
response.flags -= response::Flags::FAKE_PRIMARY_CLICKED;
|
||||
let text_clip_rect = rect;
|
||||
let text_clip_rect = inner_rect;
|
||||
let painter = ui.painter_at(text_clip_rect.expand(1.0)); // expand to avoid clipping cursor
|
||||
|
||||
if interactive && let Some(pointer_pos) = response.interact_pointer_pos() {
|
||||
@@ -581,19 +738,19 @@ impl TextEdit<'_> {
|
||||
// TODO(emilk): drag selected text to either move or clone (ctrl on windows, alt on mac)
|
||||
|
||||
let cursor_at_pointer =
|
||||
galley.cursor_from_pos(pointer_pos - rect.min + state.text_offset);
|
||||
galley.cursor_from_pos(pointer_pos - inner_rect.min + state.text_offset);
|
||||
|
||||
if ui.visuals().text_cursor.preview
|
||||
&& response.hovered()
|
||||
&& ui.input(|i| i.pointer.is_moving())
|
||||
{
|
||||
// text cursor preview:
|
||||
let cursor_rect = TSTransform::from_translation(rect.min.to_vec2())
|
||||
let cursor_rect = TSTransform::from_translation(inner_rect.min.to_vec2())
|
||||
* cursor_rect(&galley, &cursor_at_pointer, row_height);
|
||||
text_selection::visuals::paint_cursor_end(&painter, ui.visuals(), cursor_rect);
|
||||
}
|
||||
|
||||
let is_being_dragged = ui.ctx().is_being_dragged(response.id);
|
||||
let is_being_dragged = ui.is_being_dragged(response.id);
|
||||
let did_interact = state.cursor.pointer_interaction(
|
||||
ui,
|
||||
&response,
|
||||
@@ -613,44 +770,15 @@ impl TextEdit<'_> {
|
||||
ui.set_cursor_icon(CursorIcon::Text);
|
||||
}
|
||||
|
||||
let mut cursor_range = None;
|
||||
let prev_cursor_range = state.cursor.range(&galley);
|
||||
if interactive && ui.memory(|mem| mem.has_focus(id)) {
|
||||
ui.memory_mut(|mem| mem.set_focus_lock_filter(id, event_filter));
|
||||
|
||||
let default_cursor_range = if cursor_at_end {
|
||||
CCursorRange::one(galley.end())
|
||||
} else {
|
||||
CCursorRange::default()
|
||||
};
|
||||
|
||||
let (changed, new_cursor_range) = events(
|
||||
ui,
|
||||
&mut state,
|
||||
text,
|
||||
&mut galley,
|
||||
layouter,
|
||||
id,
|
||||
wrap_width,
|
||||
multiline,
|
||||
password,
|
||||
default_cursor_range,
|
||||
char_limit,
|
||||
event_filter,
|
||||
return_key,
|
||||
);
|
||||
|
||||
if changed {
|
||||
response.mark_changed();
|
||||
}
|
||||
cursor_range = Some(new_cursor_range);
|
||||
if text_changed {
|
||||
response.mark_changed();
|
||||
}
|
||||
|
||||
let mut galley_pos = align
|
||||
.align_size_within_rect(galley.size(), rect)
|
||||
.intersect(rect) // limit pos to the response rect area
|
||||
.align_size_within_rect(galley.size(), inner_rect)
|
||||
.intersect(inner_rect) // limit pos to the response rect area
|
||||
.min;
|
||||
let align_offset = rect.left_top() - galley_pos;
|
||||
let align_offset = inner_rect.left_top() - galley_pos;
|
||||
|
||||
// Visual clipping for singleline text editor with text larger than width
|
||||
if clip_text && align_offset.x == 0.0 {
|
||||
@@ -660,18 +788,18 @@ impl TextEdit<'_> {
|
||||
};
|
||||
|
||||
let mut offset_x = state.text_offset.x;
|
||||
let visible_range = offset_x..=offset_x + desired_inner_size.x;
|
||||
let visible_range = offset_x..=offset_x + inner_rect.width();
|
||||
|
||||
if !visible_range.contains(&cursor_pos) {
|
||||
if cursor_pos < *visible_range.start() {
|
||||
offset_x = cursor_pos;
|
||||
} else {
|
||||
offset_x = cursor_pos - desired_inner_size.x;
|
||||
offset_x = cursor_pos - inner_rect.width();
|
||||
}
|
||||
}
|
||||
|
||||
offset_x = offset_x
|
||||
.at_most(galley.size().x - desired_inner_size.x)
|
||||
.at_most(galley.size().x - inner_rect.width())
|
||||
.at_least(0.0);
|
||||
|
||||
state.text_offset = vec2(offset_x, align_offset.y);
|
||||
@@ -688,32 +816,7 @@ impl TextEdit<'_> {
|
||||
false
|
||||
};
|
||||
|
||||
if ui.is_rect_visible(rect) {
|
||||
if text.as_str().is_empty() && !hint_text.is_empty() {
|
||||
let hint_text_color = ui.visuals().weak_text_color();
|
||||
let hint_text_font_id = hint_text_font.unwrap_or_else(|| font_id.into());
|
||||
let galley = if multiline {
|
||||
hint_text.into_galley(
|
||||
ui,
|
||||
Some(TextWrapMode::Wrap),
|
||||
desired_inner_size.x,
|
||||
hint_text_font_id,
|
||||
)
|
||||
} else {
|
||||
hint_text.into_galley(
|
||||
ui,
|
||||
Some(TextWrapMode::Extend),
|
||||
f32::INFINITY,
|
||||
hint_text_font_id,
|
||||
)
|
||||
};
|
||||
let galley_pos = align
|
||||
.align_size_within_rect(galley.size(), rect)
|
||||
.intersect(rect)
|
||||
.min;
|
||||
painter.galley(galley_pos, galley, hint_text_color);
|
||||
}
|
||||
|
||||
if ui.is_rect_visible(inner_rect) {
|
||||
let has_focus = ui.memory(|mem| mem.has_focus(id));
|
||||
|
||||
if has_focus && let Some(cursor_range) = state.cursor.range(&galley) {
|
||||
@@ -721,45 +824,11 @@ impl TextEdit<'_> {
|
||||
paint_text_selection(&mut galley, ui.visuals(), &cursor_range, None);
|
||||
}
|
||||
|
||||
// Allocate additional space if edits were made this frame that changed the size. This is important so that,
|
||||
// if there's a ScrollArea, it can properly scroll to the cursor.
|
||||
// Condition `!clip_text` is important to avoid breaking layout for `TextEdit::singleline` (PR #5640)
|
||||
if !clip_text
|
||||
&& let extra_size = galley.size() - rect.size()
|
||||
&& (extra_size.x > 0.0 || extra_size.y > 0.0)
|
||||
{
|
||||
match ui.layout().main_dir() {
|
||||
crate::Direction::LeftToRight | crate::Direction::TopDown => {
|
||||
ui.allocate_rect(
|
||||
Rect::from_min_size(outer_rect.max, extra_size),
|
||||
Sense::hover(),
|
||||
);
|
||||
}
|
||||
crate::Direction::RightToLeft => {
|
||||
ui.allocate_rect(
|
||||
Rect::from_min_size(
|
||||
emath::pos2(outer_rect.min.x - extra_size.x, outer_rect.max.y),
|
||||
extra_size,
|
||||
),
|
||||
Sense::hover(),
|
||||
);
|
||||
}
|
||||
crate::Direction::BottomUp => {
|
||||
ui.allocate_rect(
|
||||
Rect::from_min_size(
|
||||
emath::pos2(outer_rect.min.x, outer_rect.max.y - extra_size.y),
|
||||
extra_size,
|
||||
),
|
||||
Sense::hover(),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Avoid an ID shift during this pass if the textedit grow
|
||||
ui.skip_ahead_auto_ids(1);
|
||||
}
|
||||
|
||||
painter.galley(galley_pos, Arc::clone(&galley), text_color);
|
||||
painter.galley(
|
||||
galley_pos - vec2(galley.rect.left(), 0.0),
|
||||
Arc::clone(&galley),
|
||||
text_color,
|
||||
);
|
||||
|
||||
if has_focus && let Some(cursor_range) = state.cursor.range(&galley) {
|
||||
let primary_cursor_rect = cursor_rect(&galley, &cursor_range.primary, row_height)
|
||||
@@ -767,7 +836,7 @@ impl TextEdit<'_> {
|
||||
|
||||
if response.changed() || selection_changed {
|
||||
// Scroll to keep primary cursor in view:
|
||||
ui.scroll_to_rect(primary_cursor_rect + margin, None);
|
||||
ui.scroll_to_rect(primary_cursor_rect, None);
|
||||
}
|
||||
|
||||
if text.is_mutable() && interactive {
|
||||
@@ -796,9 +865,9 @@ impl TextEdit<'_> {
|
||||
.layer_transform_to_global(ui.layer_id())
|
||||
.unwrap_or_default();
|
||||
|
||||
ui.ctx().output_mut(|o| {
|
||||
ui.output_mut(|o| {
|
||||
o.ime = Some(crate::output::IMEOutput {
|
||||
rect: to_global * rect,
|
||||
rect: to_global * inner_rect,
|
||||
cursor_rect: to_global * primary_cursor_rect,
|
||||
});
|
||||
});
|
||||
@@ -846,24 +915,22 @@ impl TextEdit<'_> {
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let role = if password {
|
||||
accesskit::Role::PasswordInput
|
||||
} else if multiline {
|
||||
accesskit::Role::MultilineTextInput
|
||||
} else {
|
||||
accesskit::Role::TextInput
|
||||
};
|
||||
let role = if password {
|
||||
accesskit::Role::PasswordInput
|
||||
} else if multiline {
|
||||
accesskit::Role::MultilineTextInput
|
||||
} else {
|
||||
accesskit::Role::TextInput
|
||||
};
|
||||
|
||||
crate::text_selection::accesskit_text::update_accesskit_for_text_widget(
|
||||
ui.ctx(),
|
||||
id,
|
||||
cursor_range,
|
||||
role,
|
||||
TSTransform::from_translation(galley_pos.to_vec2()),
|
||||
&galley,
|
||||
);
|
||||
}
|
||||
crate::text_selection::accesskit_text::update_accesskit_for_text_widget(
|
||||
ui.ctx(),
|
||||
id,
|
||||
cursor_range,
|
||||
role,
|
||||
TSTransform::from_translation(galley_pos.to_vec2()),
|
||||
&galley,
|
||||
);
|
||||
|
||||
TextEditOutput {
|
||||
response,
|
||||
@@ -911,7 +978,7 @@ fn events(
|
||||
event_filter: EventFilter,
|
||||
return_key: Option<KeyboardShortcut>,
|
||||
) -> (bool, CCursorRange) {
|
||||
let os = ui.ctx().os();
|
||||
let os = ui.os();
|
||||
|
||||
let mut cursor_range = state.cursor.range(galley).unwrap_or(default_cursor_range);
|
||||
|
||||
@@ -930,13 +997,7 @@ fn events(
|
||||
|
||||
let mut any_change = false;
|
||||
|
||||
let mut events = ui.input(|i| i.filtered_events(&event_filter));
|
||||
|
||||
if state.ime_enabled {
|
||||
remove_ime_incompatible_events(&mut events);
|
||||
// Process IME events first:
|
||||
events.sort_by_key(|e| !matches!(e, Event::Ime(_)));
|
||||
}
|
||||
let events = ui.input(|i| i.filtered_events(&event_filter));
|
||||
|
||||
for event in &events {
|
||||
let did_mutate_text = match event {
|
||||
@@ -1066,26 +1127,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 +1165,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))
|
||||
@@ -1159,27 +1230,6 @@ fn events(
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
fn remove_ime_incompatible_events(events: &mut Vec<Event>) {
|
||||
// Remove key events which cause problems while 'IME' is being used.
|
||||
// See https://github.com/emilk/egui/pull/4509
|
||||
events.retain(|event| {
|
||||
!matches!(
|
||||
event,
|
||||
Event::Key { repeat: true, .. }
|
||||
| Event::Key {
|
||||
key: Key::Backspace
|
||||
| Key::ArrowUp
|
||||
| Key::ArrowDown
|
||||
| Key::ArrowLeft
|
||||
| Key::ArrowRight,
|
||||
..
|
||||
}
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Returns `Some(new_cursor)` if we did mutate `text`.
|
||||
fn check_for_mutating_key_press(
|
||||
os: OperatingSystem,
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::text::CCursorRange;
|
||||
/// The output from a [`TextEdit`](crate::TextEdit).
|
||||
pub struct TextEditOutput {
|
||||
/// The interaction response.
|
||||
pub response: crate::Response,
|
||||
pub response: crate::AtomLayoutResponse,
|
||||
|
||||
/// How the text was displayed.
|
||||
pub galley: Arc<crate::Galley>,
|
||||
|
||||
@@ -25,10 +25,10 @@ crate-type = ["cdylib", "rlib"]
|
||||
[features]
|
||||
default = ["wgpu", "persistence"]
|
||||
|
||||
# image_viewer adds about 0.9 MB of WASM
|
||||
web_app = ["http", "persistence"]
|
||||
|
||||
accessibility_inspector = ["dep:accesskit", "dep:accesskit_consumer", "eframe/accesskit"]
|
||||
easymark = [] # easymark is off by default, because it a pretty shitty markup language
|
||||
http = ["ehttp", "image/jpeg", "poll-promise", "egui_extras/image"]
|
||||
image_viewer = ["image/jpeg", "egui_extras/all_loaders", "rfd"]
|
||||
persistence = ["eframe/persistence", "egui_extras/serde", "egui/persistence", "serde"]
|
||||
@@ -42,15 +42,15 @@ wayland = ["eframe/wayland"]
|
||||
x11 = ["eframe/x11"]
|
||||
|
||||
[dependencies]
|
||||
chrono = { workspace = true, features = ["js-sys", "wasmbind"] }
|
||||
eframe = { workspace = true, default-features = false, features = ["web_screen_reader"] }
|
||||
egui = { workspace = true, features = ["callstack", "default"] }
|
||||
egui_demo_lib = { workspace = true, features = ["default", "chrono"] }
|
||||
egui_demo_lib = { workspace = true, features = ["default", "jiff"] }
|
||||
egui_extras = { workspace = true, features = ["default", "image"] }
|
||||
image = { workspace = true, default-features = false, features = [
|
||||
# Ensure we can display the test images
|
||||
"png",
|
||||
] }
|
||||
jiff = { workspace = true, features = ["std", "tz-system", "js"] }
|
||||
log.workspace = true
|
||||
profiling.workspace = true
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -40,8 +40,8 @@ impl Custom3d {
|
||||
|
||||
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("custom3d"),
|
||||
bind_group_layouts: &[&bind_group_layout],
|
||||
push_constant_ranges: &[],
|
||||
bind_group_layouts: &[Some(&bind_group_layout)],
|
||||
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();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -9,9 +9,10 @@ pub use wrap_app::{Anchor, WrapApp};
|
||||
|
||||
/// Time of day as seconds since midnight. Used for clock in demo app.
|
||||
pub(crate) fn seconds_since_midnight() -> f64 {
|
||||
use chrono::Timelike as _;
|
||||
let time = chrono::Local::now().time();
|
||||
time.num_seconds_from_midnight() as f64 + 1e-9 * (time.nanosecond() as f64)
|
||||
jiff::Zoned::now()
|
||||
.time()
|
||||
.duration_since(jiff::civil::Time::midnight())
|
||||
.as_secs_f64()
|
||||
}
|
||||
|
||||
/// Trait that wraps different parts of the demo app.
|
||||
|
||||
@@ -8,12 +8,14 @@ use core::any::Any;
|
||||
|
||||
use crate::DemoApp;
|
||||
|
||||
#[cfg(feature = "easymark")]
|
||||
#[derive(Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
struct EasyMarkApp {
|
||||
editor: egui_demo_lib::easy_mark::EasyMarkEditor,
|
||||
}
|
||||
|
||||
#[cfg(feature = "easymark")]
|
||||
impl DemoApp for EasyMarkApp {
|
||||
fn demo_ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||||
self.editor.panels(ui);
|
||||
@@ -152,12 +154,18 @@ enum Command {
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct State {
|
||||
demo: DemoWindows,
|
||||
|
||||
#[cfg(feature = "easymark")]
|
||||
easy_mark_editor: EasyMarkApp,
|
||||
|
||||
#[cfg(feature = "http")]
|
||||
http: crate::apps::HttpApp,
|
||||
|
||||
#[cfg(feature = "image_viewer")]
|
||||
image_viewer: crate::apps::ImageViewer,
|
||||
|
||||
pub clock: FractalClockApp,
|
||||
|
||||
rendering_test: ColorTestApp,
|
||||
|
||||
selected_anchor: Anchor,
|
||||
@@ -212,6 +220,7 @@ impl WrapApp {
|
||||
Anchor::Demo,
|
||||
&mut self.state.demo as &mut dyn DemoApp,
|
||||
),
|
||||
#[cfg(feature = "easymark")]
|
||||
(
|
||||
"🖹 EasyMark editor",
|
||||
Anchor::EasyMarkEditor,
|
||||
@@ -400,6 +409,8 @@ impl WrapApp {
|
||||
}
|
||||
|
||||
fn bar_contents(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cmd: &mut Command) {
|
||||
ui.add_space(8.0);
|
||||
|
||||
egui::widgets::global_theme_preference_switch(ui);
|
||||
|
||||
ui.separator();
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5878bc5beaf4406c24f23d900aa9ac7c5507e44cb3ade83b743b8b62e7da1615
|
||||
size 335355
|
||||
oid sha256:63021012cccfca02d09aa424333453140ae4da3ae58fa32b422f6152ba25741c
|
||||
size 335394
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:706ad012e52a8c51175b050b985cca88e2cb306b24f618b7391641397d17cd28
|
||||
size 92804
|
||||
oid sha256:4470063fe210d2e5170d6609c2603fff1984b8ee76fb65a1f60a1c4cfdf46ce8
|
||||
size 92796
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4135662f2b60a10ef8c3b155172d7a3edcf24a625d8286aeaad0614aa8819893
|
||||
size 169604
|
||||
oid sha256:b9ad01a55950f96a3ae9e48a2c026143d11ffee62bff4f83b4529cd884ce11f0
|
||||
size 169683
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:509020d8885b718900e534c9948cb95ae88e1eee9e113bdfb77a2f75b9a68f7b
|
||||
size 96703
|
||||
oid sha256:6030f2f3da3dbbdf8bf3eaf429f222acffb624c7696b654d8b6e64273d49be58
|
||||
size 99008
|
||||
|
||||
@@ -11,7 +11,7 @@ readme = "README.md"
|
||||
repository = "https://github.com/emilk/egui/tree/main/crates/egui_demo_lib"
|
||||
categories = ["gui", "graphics"]
|
||||
keywords = ["glow", "egui", "gui", "gamedev"]
|
||||
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml", "data/*"]
|
||||
include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml", "data/*"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -26,7 +26,7 @@ rustdoc-args = ["--generate-link-to-definition"]
|
||||
[features]
|
||||
default = []
|
||||
|
||||
chrono = ["egui_extras/datepicker", "dep:chrono"]
|
||||
jiff = ["egui_extras/datepicker", "dep:jiff"]
|
||||
|
||||
## Allow serialization using [`serde`](https://docs.rs/serde).
|
||||
serde = ["egui/serde", "dep:serde", "egui_extras/serde"]
|
||||
@@ -42,7 +42,7 @@ egui_extras = { workspace = true, features = ["image", "svg"] }
|
||||
unicode_names2.workspace = true # this old version has fewer dependencies
|
||||
|
||||
#! ### Optional dependencies
|
||||
chrono = { workspace = true, optional = true, features = ["js-sys", "wasmbind"] }
|
||||
jiff = { workspace = true, optional = true, features = ["std", "js"] }
|
||||
## Enable this when generating docs.
|
||||
document-features = { workspace = true, optional = true }
|
||||
serde = { workspace = true, optional = true }
|
||||
|
||||
21
crates/egui_demo_lib/data/egui-logo.svg
Normal file
@@ -0,0 +1,21 @@
|
||||
<svg width="1165" height="366" viewBox="0 0 1165 366" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2005_93)">
|
||||
<mask id="mask0_2005_93" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="334" y="0" width="831" height="366">
|
||||
<path d="M1165 0H334V365.17H1165V0Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_2005_93)">
|
||||
<path d="M334.059 166.33C334.679 134.02 349.889 103.38 376.099 84.4502C390.759 73.8602 409.699 68.8302 427.749 68.9302C472.239 69.1802 500.199 89.0902 514.439 126.26C518.609 137.17 520.839 145.47 521.289 157.13C521.629 166.3 521.789 178.32 521.829 183.4C521.829 184.74 520.769 185.81 519.429 185.81H382.519C380.709 185.81 379.249 187.31 379.309 189.12C380.349 220.05 396.199 237.77 427.749 238.44C447.819 238.87 463.969 230.46 470.749 216.17C471.259 215.08 472.379 214.41 473.599 214.41H517.259C518.809 214.41 519.989 215.88 519.589 217.41C510.809 251.9 478.299 279.88 427.749 280.6C359.269 280.43 338.449 230.44 334.299 182.61C333.939 178.51 333.939 172.29 334.049 166.32V166.34L334.059 166.33ZM469.049 146.33C470.989 146.33 472.479 144.62 472.229 142.69C469.219 119.35 452.109 106.3 427.969 106.3C400.019 106.3 385.689 118.56 380.089 142.35C379.619 144.38 381.119 146.32 383.199 146.32H469.049V146.33Z" fill="white"/>
|
||||
<path d="M685.599 75.4002V79.9202C685.599 82.3602 682.999 83.9002 680.859 82.7302C670.869 75.0802 658.989 68.9002 637.819 68.9102C591.729 68.9602 552.329 100.48 547.649 145.55C546.819 153.48 546.669 183.26 546.859 186.07C550.349 242.31 566.189 280.84 636.229 280.84C661.839 280.84 670.739 272.74 681.219 266.01C683.319 265.14 685.629 266.71 685.629 268.98V317.05C712.179 317.05 733.709 289.78 733.709 263.22V75.4002C733.709 73.6302 732.279 72.1902 730.499 72.1902H688.839C687.069 72.1902 685.629 73.6202 685.629 75.3902L685.599 75.4002ZM685.229 190.32C685.229 195.53 684.519 202.49 682.979 207.58C676.579 228.66 662.369 239.82 636.519 239.82C612.679 239.82 595.699 226.2 592.379 202.84C592.029 200.44 591.899 198.01 591.899 195.58V155.56C591.899 150.25 592.609 144.96 594.149 139.88C600.549 118.8 614.759 106.82 640.609 106.82C664.449 106.82 681.429 121.26 684.749 144.63C685.089 147.03 685.229 149.46 685.229 151.89V190.34V190.32Z" fill="white"/>
|
||||
<path d="M561.44 317.09H685.63V320.3C685.63 345.09 665.54 365.17 640.76 365.17H561.44C559.67 365.17 558.23 363.73 558.23 361.96V320.3C558.23 318.53 559.67 317.09 561.44 317.09Z" fill="white"/>
|
||||
<path d="M1097.7 72.1301C1099.47 72.1301 1100.92 73.5301 1100.92 75.3001C1100.92 86.6501 1100.9 205.37 1100.9 226.49C1100.9 228.26 1102.34 229.7 1104.11 229.7H1161.79C1163.56 229.7 1165 231.14 1165 232.91V274.78C1165 276.55 1163.57 277.99 1161.79 277.99H991.928C990.158 277.99 988.718 276.56 988.718 274.78V232.9C988.718 231.13 990.148 229.69 991.928 229.69H1049.61C1051.38 229.69 1052.81 228.26 1052.81 226.48L1052.78 120.18C1052.78 118.41 1051.34 116.97 1049.57 116.97H991.878C990.108 116.97 988.668 115.54 988.668 113.76V75.3101C988.668 73.5401 990.098 72.1001 991.878 72.1001H1097.7V72.1201V72.1301Z" fill="white"/>
|
||||
<path d="M910.9 72.1299C909.13 72.1299 907.69 73.5699 907.69 75.3399V192.16C907.69 195.39 907.46 198.84 907.07 202.11C906.32 208.25 904.46 214.24 901.28 219.55C893.34 232.89 879.58 239.42 858.99 239.82C835.72 240.27 818.54 225.08 815.21 202.11C814.87 199.76 814.39 194.57 814.39 192.16V75.3399C814.39 73.5699 812.96 72.1299 811.18 72.1299H768.39C766.62 72.1299 765.18 73.5699 765.18 75.3399V188.89C765.18 240.58 788.39 282.7 860.25 281.02C883.37 277.82 890.5 272.68 903.44 266.05C905.56 264.96 908.09 266.52 908.09 268.91L907.97 274.61C907.97 276.38 909.41 277.82 911.18 277.82H952.89C954.66 277.82 956.1 276.38 956.1 274.61V75.3399C956.1 73.5699 954.67 72.1299 952.89 72.1299H910.9Z" fill="white"/>
|
||||
<path d="M1097.66 0H1056C1054.22 0 1052.79 1.43 1052.79 3.21V44.88C1052.79 46.65 1054.22 48.09 1056 48.09H1097.66C1099.43 48.09 1100.87 46.66 1100.87 44.88V3.21C1100.87 1.44 1099.43 0 1097.66 0Z" fill="white"/>
|
||||
</g>
|
||||
<path d="M165.96 72C171.15 72 175.95 74.8 178.55 79.34L229.02 167.66C231.62 172.2 231.62 177.8 229.02 182.34L178.55 270.66C175.95 275.2 171.15 278 165.96 278H65.01C59.82 278 55.02 275.2 52.42 270.66L1.95 182.34C-0.65 177.8 -0.65 172.2 1.95 167.66L52.43 79.34C55.03 74.8 59.82 72 65.02 72H165.97H165.96ZM99.88 249.29C99.88 251.36 101.56 253.03 103.63 253.03H127.56C129.51 253.03 131.09 251.45 131.09 249.5V221.82C131.09 204.58 117.12 190.61 99.88 190.61V249.29ZM40.99 159.39C39.04 159.39 37.46 160.97 37.46 162.92V186.85C37.46 188.92 39.14 190.6 41.21 190.6H99.89C99.89 173.36 85.92 159.39 68.68 159.39H41H40.99ZM131.09 159.39C131.09 176.63 145.06 190.6 162.3 190.6H189.98C191.93 190.6 193.51 189.02 193.51 187.07V163.14C193.51 161.07 191.83 159.39 189.76 159.39H131.08H131.09ZM103.41 96.97C101.46 96.97 99.88 98.55 99.88 100.5V128.18C99.88 145.42 113.85 159.39 131.09 159.39V100.71C131.09 98.64 129.41 96.96 127.34 96.96H103.41V96.97Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2005_93">
|
||||
<rect width="1165" height="366" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 1.7 KiB |
5
crates/egui_demo_lib/data/icon.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16">
|
||||
<rect x="0.1" y="0.1" width="15.8" height="15.8" rx="2.75714" fill="black" data-darkreader-inline-fill="" style="--darkreader-inline-fill: var(--darkreader-background-000000, #000000);"/>
|
||||
<rect x="0.1" y="0.1" width="15.8" height="15.8" rx="2.75714" stroke="white" stroke-width="0.2" data-darkreader-inline-stroke="" style="--darkreader-inline-stroke: var(--darkreader-text-ffffff, #e8e6e3);"/>
|
||||
<path d="M10.6112 2.54492C10.8843 2.54492 11.1367 2.69172 11.2733 2.93066L13.9286 7.5752C14.065 7.81401 14.0649 8.10877 13.9286 8.34766L11.2733 12.9932C11.1367 13.232 10.8843 13.3789 10.6112 13.3789H5.30162C5.02857 13.3789 4.77605 13.2321 4.63951 12.9932L1.98521 8.34766C1.8487 8.10872 1.84866 7.81412 1.98521 7.5752L4.63951 2.93066C4.77605 2.69176 5.02855 2.54497 5.30162 2.54492H10.6112ZM7.13658 11.8691C7.13674 11.9778 7.22514 12.0654 7.33384 12.0654H8.59263C8.69507 12.0652 8.77818 11.9824 8.77818 11.8799V10.4238C8.77797 9.51742 8.04301 8.78231 7.13658 8.78223V11.8691ZM4.03892 7.14062C3.93634 7.1407 3.85337 7.22456 3.85337 7.32715V8.58594C3.85355 8.6946 3.94195 8.78223 4.05064 8.78223H7.13658C7.13637 7.87582 6.40142 7.1407 5.49498 7.14062H4.03892ZM8.7772 7.14062C8.7772 8.04726 9.51217 8.78223 10.4188 8.78223H11.8749C11.9773 8.78201 12.0604 8.69918 12.0604 8.59668V7.33789C12.0604 7.22913 11.9719 7.14062 11.8631 7.14062H8.7772ZM7.32212 3.85742C7.21955 3.85749 7.13658 3.94135 7.13658 4.04395V5.49902C7.13658 6.40566 7.87154 7.14062 8.77818 7.14062V4.05469C8.77816 3.94591 8.68971 3.85742 8.58091 3.85742H7.32212Z" fill="white" data-darkreader-inline-fill="" style="--darkreader-inline-fill: var(--darkreader-background-ffffff, #181a1b);"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 44 44" width="32" height="32">
|
||||
<g fill="none" stroke="white" stroke-width="3">
|
||||
<circle cx="22" cy="22" r="19"/>
|
||||
<path d="M 22,2 V 41"/>
|
||||
<path d="M 22,2 V 22" transform="rotate(135, 22, 22)"/>
|
||||
<path d="M 22,2 V 22" transform="rotate(225, 22, 22)"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 437 B |
@@ -27,20 +27,30 @@ impl crate::View for About {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
use egui::special_emojis::{OS_APPLE, OS_LINUX, OS_WINDOWS};
|
||||
|
||||
ui.heading("egui");
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add_space(4.0);
|
||||
let egui_icon = egui::include_image!("../../data/egui-logo.svg");
|
||||
ui.add(
|
||||
egui::Image::new(egui_icon.clone())
|
||||
.max_height(30.0)
|
||||
.tint(ui.visuals().strong_text_color()),
|
||||
);
|
||||
ui.add_space(4.0);
|
||||
});
|
||||
|
||||
ui.label(format!(
|
||||
"egui is an immediate mode GUI library written in Rust. egui runs both on the web and natively on {}{}{}. \
|
||||
On the web it is compiled to WebAssembly and rendered with WebGL.{}",
|
||||
"egui is an immediate mode GUI library written in Rust. egui runs natively on {}{}{}, and \
|
||||
on the web it is compiled to WebAssembly and rendered with WebGL or WebGPU.{}",
|
||||
OS_APPLE, OS_LINUX, OS_WINDOWS,
|
||||
if cfg!(target_arch = "wasm32") {
|
||||
" Everything you see is rendered as textured triangles. There is no DOM, HTML, JS or CSS. Just Rust."
|
||||
} else {""}
|
||||
));
|
||||
ui.label("egui is designed to be easy to use, portable, and fast.");
|
||||
|
||||
ui.add_space(12.0);
|
||||
ui.label("egui is easy to use, portable, and fast.");
|
||||
|
||||
ui.heading("Immediate mode");
|
||||
ui.add_space(12.0);
|
||||
about_immediate_mode(ui);
|
||||
|
||||
ui.add_space(12.0);
|
||||
@@ -52,12 +62,12 @@ impl crate::View for About {
|
||||
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("egui development is sponsored by ");
|
||||
ui.weak("egui development is sponsored by ");
|
||||
ui.hyperlink_to("Rerun.io", "https://www.rerun.io/");
|
||||
ui.label(", a startup building an SDK for visualizing streams of multimodal data. ");
|
||||
ui.label("For an example of a real-world egui app, see ");
|
||||
ui.weak(", a startup building a data platform for robotics. ");
|
||||
ui.weak("For an example of a professional egui app, run ");
|
||||
ui.hyperlink_to("rerun.io/viewer", "https://www.rerun.io/viewer");
|
||||
ui.label(" (runs in your browser).");
|
||||
ui.weak(" (in your browser!).");
|
||||
});
|
||||
|
||||
ui.add_space(12.0);
|
||||
@@ -72,11 +82,9 @@ fn about_immediate_mode(ui: &mut egui::Ui) {
|
||||
ui.style_mut().spacing.interact_size.y = 0.0; // hack to make `horizontal_wrapped` work better with text.
|
||||
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("Immediate mode is a GUI paradigm that lets you create a GUI with less code and simpler control flow. For example, this is how you create a ");
|
||||
let _ = ui.small_button("button");
|
||||
ui.label(" in egui:");
|
||||
});
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("This is how you create a button in egui:");
|
||||
});
|
||||
|
||||
ui.add_space(8.0);
|
||||
crate::rust_view_ui(
|
||||
|
||||
@@ -119,21 +119,30 @@ impl Default for DemoGroups {
|
||||
}
|
||||
|
||||
impl DemoGroups {
|
||||
pub fn about_egui_checkbox(&mut self, ui: &mut Ui, open: &mut BTreeSet<String>) {
|
||||
let Self { about, .. } = self;
|
||||
let mut is_open = open.contains(about.name());
|
||||
ui.toggle_value(&mut is_open, about.name());
|
||||
set_open(open, about.name(), is_open);
|
||||
}
|
||||
|
||||
pub fn checkboxes(&mut self, ui: &mut Ui, open: &mut BTreeSet<String>) {
|
||||
let Self {
|
||||
about,
|
||||
about: _,
|
||||
demos,
|
||||
tests,
|
||||
} = self;
|
||||
|
||||
{
|
||||
let mut is_open = open.contains(about.name());
|
||||
ui.toggle_value(&mut is_open, about.name());
|
||||
set_open(open, about.name(), is_open);
|
||||
}
|
||||
ui.separator();
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.strong("Demos");
|
||||
});
|
||||
demos.checkboxes(ui, open);
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.strong("Tests");
|
||||
});
|
||||
tests.checkboxes(ui, open);
|
||||
}
|
||||
|
||||
@@ -267,22 +276,20 @@ impl DemoWindows {
|
||||
.default_size(160.0)
|
||||
.min_size(160.0)
|
||||
.show_inside(ui, |ui| {
|
||||
ui.add_space(4.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.heading("✒ egui demos");
|
||||
ui.vertical_centered_justified(|ui| {
|
||||
ui.add_space(4.0);
|
||||
ui.add(
|
||||
egui::Image::new(egui::include_image!("../../data/egui-logo.svg"))
|
||||
.max_height(32.0)
|
||||
.tint(ui.visuals().strong_text_color()),
|
||||
);
|
||||
|
||||
ui.add_space(4.0);
|
||||
|
||||
self.groups.about_egui_checkbox(ui, &mut self.open);
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
use egui::special_emojis::GITHUB;
|
||||
ui.hyperlink_to(
|
||||
format!("{GITHUB} egui on GitHub"),
|
||||
"https://github.com/emilk/egui",
|
||||
);
|
||||
ui.hyperlink_to(
|
||||
"@ernerfeldt.bsky.social",
|
||||
"https://bsky.app/profile/ernerfeldt.bsky.social",
|
||||
);
|
||||
ui.add_space(4.0);
|
||||
|
||||
ui.separator();
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ impl crate::View for SvgTest {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
let Self { color } = self;
|
||||
ui.color_edit_button_srgba(color);
|
||||
let img_src = egui::include_image!("../../../data/peace.svg");
|
||||
let img_src = egui::include_image!("../../../data/icon.svg");
|
||||
|
||||
// First paint a small version, sized the same as the source…
|
||||
ui.add(
|
||||
|
||||
@@ -297,6 +297,7 @@ fn rect_shape_ui(ui: &mut egui::Ui, shape: &mut RectShape) {
|
||||
blur_width,
|
||||
round_to_pixels,
|
||||
brush: _,
|
||||
angle: _,
|
||||
} = shape;
|
||||
|
||||
let round_to_pixels = round_to_pixels.get_or_insert(true);
|
||||
|
||||
@@ -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 _;
|
||||
|
||||
@@ -19,11 +19,11 @@ pub struct WidgetGallery {
|
||||
color: egui::Color32,
|
||||
animate_progress_bar: bool,
|
||||
|
||||
#[cfg(feature = "chrono")]
|
||||
#[cfg(feature = "jiff")]
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
date: Option<chrono::NaiveDate>,
|
||||
date: Option<jiff::civil::Date>,
|
||||
|
||||
#[cfg(feature = "chrono")]
|
||||
#[cfg(feature = "jiff")]
|
||||
with_date_button: bool,
|
||||
}
|
||||
|
||||
@@ -39,19 +39,19 @@ impl Default for WidgetGallery {
|
||||
string: Default::default(),
|
||||
color: egui::Color32::LIGHT_BLUE.linear_multiply(0.5),
|
||||
animate_progress_bar: false,
|
||||
#[cfg(feature = "chrono")]
|
||||
#[cfg(feature = "jiff")]
|
||||
date: None,
|
||||
#[cfg(feature = "chrono")]
|
||||
#[cfg(feature = "jiff")]
|
||||
with_date_button: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetGallery {
|
||||
#[allow(clippy::allow_attributes, unused_mut)] // if not chrono
|
||||
#[allow(clippy::allow_attributes, unused_mut)] // if not jiff
|
||||
#[inline]
|
||||
pub fn with_date_button(mut self, _with_date_button: bool) -> Self {
|
||||
#[cfg(feature = "chrono")]
|
||||
#[cfg(feature = "jiff")]
|
||||
{
|
||||
self.with_date_button = _with_date_button;
|
||||
}
|
||||
@@ -140,9 +140,9 @@ impl WidgetGallery {
|
||||
string,
|
||||
color,
|
||||
animate_progress_bar,
|
||||
#[cfg(feature = "chrono")]
|
||||
#[cfg(feature = "jiff")]
|
||||
date,
|
||||
#[cfg(feature = "chrono")]
|
||||
#[cfg(feature = "jiff")]
|
||||
with_date_button,
|
||||
} = self;
|
||||
|
||||
@@ -229,7 +229,7 @@ impl WidgetGallery {
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("Image", "Image"));
|
||||
let egui_icon = egui::include_image!("../../data/icon.png");
|
||||
let egui_icon = egui::include_image!("../../data/icon.svg");
|
||||
ui.add(egui::Image::new(egui_icon.clone()));
|
||||
ui.end_row();
|
||||
|
||||
@@ -237,17 +237,14 @@ impl WidgetGallery {
|
||||
"Button with image",
|
||||
"Button::image_and_text",
|
||||
));
|
||||
if ui
|
||||
.add(egui::Button::image_and_text(egui_icon, "Click me!"))
|
||||
.clicked()
|
||||
{
|
||||
if ui.button((egui_icon, "Click me!")).clicked() {
|
||||
*boolean = !*boolean;
|
||||
}
|
||||
ui.end_row();
|
||||
|
||||
#[cfg(feature = "chrono")]
|
||||
#[cfg(feature = "jiff")]
|
||||
if *with_date_button {
|
||||
let date = date.get_or_insert_with(|| chrono::offset::Utc::now().date_naive());
|
||||
let date = date.get_or_insert_with(|| jiff::Zoned::now().date());
|
||||
ui.add(doc_link_label_with_crate(
|
||||
"egui_extras",
|
||||
"DatePickerButton",
|
||||
@@ -305,7 +302,7 @@ fn doc_link_label_with_crate<'a>(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "chrono")]
|
||||
#[cfg(feature = "jiff")]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -317,7 +314,7 @@ mod tests {
|
||||
pub fn should_match_screenshot() {
|
||||
let mut demo = WidgetGallery {
|
||||
// If we don't set a fixed date, the snapshot test will fail.
|
||||
date: Some(chrono::NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()),
|
||||
date: Some(jiff::civil::date(2024, 1, 1)),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
size 26977
|
||||
oid sha256:24f4a9745c60c0353ece5f8fc48200671dcb185f4f0b964bbe66bf4a2fe71d7a
|
||||
size 27067
|
||||
|
||||
@@ -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:b849adf0ff9a06e1f7bfa22ef32b13224c7e62429cf675286110729156434d12
|
||||
size 76531
|
||||
|
||||
@@ -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
|
||||
oid sha256:8c630df841e98043132b920338793745549116890c1bc52d8d10b0def896a1e6
|
||||
size 114409
|
||||
|
||||
@@ -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:56b44d26946770c0878e11e3197633697ad339a7e8fcffe7279a6b4c45cd3582
|
||||
size 65384
|
||||
oid sha256:b6b4c2e55c02fa4caf5f9f8bd2d8c0311cc4cbcf1fc2f568fe112e8e6125c675
|
||||
size 65308
|
||||
|
||||