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

Merge branch 'main' into lucas/experiments/measure-widget-size

# Conflicts:
#	crates/egui/src/atomics/atom.rs
#	crates/egui/src/atomics/atom_kind.rs
#	crates/egui/src/atomics/atom_layout.rs
#	crates/egui/src/containers/sides.rs
#	crates/egui/src/placer.rs
#	crates/egui/src/widgets/label.rs
#	crates/egui/src/widgets/selected_label.rs
#	crates/egui_demo_lib/src/demo/dancing_strings.rs
#	crates/egui_extras/src/layout.rs
#	crates/epaint/src/text/text_layout_types.rs
This commit is contained in:
lucasmerlin
2025-07-14 15:35:38 +02:00
336 changed files with 3896 additions and 1837 deletions

View File

@@ -6,6 +6,12 @@ This file is updated upon each release.
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
## 0.32.0 - 2025-07-10
* Fix semi-transparent colors appearing too bright [#5824](https://github.com/emilk/egui/pull/5824) by [@emilk](https://github.com/emilk)
* Remove things that have been deprecated for over a year [#7099](https://github.com/emilk/egui/pull/7099) by [@emilk](https://github.com/emilk)
* Make `Hsva` derive serde [#7132](https://github.com/emilk/egui/pull/7132) by [@bircni](https://github.com/bircni)
## 0.31.1 - 2025-03-05
Nothing new

View File

@@ -1,4 +1,4 @@
use super::{linear_f32_from_linear_u8, linear_u8_from_linear_f32, Color32, Hsva, HsvaGamma, Rgba};
use super::{Color32, Hsva, HsvaGamma, Rgba, linear_f32_from_linear_u8, linear_u8_from_linear_f32};
use cint::{Alpha, ColorInterop, EncodedSrgb, Hsv, LinearSrgb, PremultipliedAlpha};
// ---- Color32 ----

View File

@@ -1,4 +1,4 @@
use crate::{fast_round, linear_f32_from_linear_u8, Rgba};
use crate::{Rgba, fast_round, linear_f32_from_linear_u8};
/// This format is used for space-efficient color representation (32 bits).
///

View File

@@ -1,5 +1,5 @@
use crate::{
gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_u8_from_linear_f32, Color32, Rgba,
Color32, Rgba, gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_u8_from_linear_f32,
};
/// Hue, saturation, value, alpha. All in the range [0, 1].

View File

@@ -1,4 +1,4 @@
use crate::{gamma_from_linear, linear_from_gamma, Color32, Hsva, Rgba};
use crate::{Color32, Hsva, Rgba, gamma_from_linear, linear_from_gamma};
/// Like Hsva but with the `v` value (brightness) being gamma corrected
/// so that it is somewhat perceptually even.

View File

@@ -7,6 +7,33 @@ This file is updated upon each release.
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
## 0.32.0 - 2025-07-10
### ⭐ Added
* Add pointer events and focus handling for apps run in a Shadow DOM [#5627](https://github.com/emilk/egui/pull/5627) by [@xxvvii](https://github.com/xxvvii)
* MacOS: Add `movable_by_window_background` option to viewport [#5412](https://github.com/emilk/egui/pull/5412) by [@jim-ec](https://github.com/jim-ec)
* Add macOS-specific `has_shadow` and `with_has_shadow` to ViewportBuilder [#6850](https://github.com/emilk/egui/pull/6850) by [@gaelanmcmillan](https://github.com/gaelanmcmillan)
* Add external eventloop support [#6750](https://github.com/emilk/egui/pull/6750) by [@wpbrown](https://github.com/wpbrown)
### 🔧 Changed
* Update MSRV to 1.85 [#7279](https://github.com/emilk/egui/pull/7279) by [@emilk](https://github.com/emilk)
* Use Rust edition 2024 [#7280](https://github.com/emilk/egui/pull/7280) by [@emilk](https://github.com/emilk)
* Rename `should_propagate_event` and add `should_prevent_default` [#5779](https://github.com/emilk/egui/pull/5779) by [@th0rex](https://github.com/th0rex)
* Clarify platform-specific details for `Viewport` positioning [#5715](https://github.com/emilk/egui/pull/5715) by [@aspiringLich](https://github.com/aspiringLich)
* Enhance stability on Windows [#5723](https://github.com/emilk/egui/pull/5723) by [@rustbasic](https://github.com/rustbasic)
* Set `web-sys` min version to `0.3.73` [#5862](https://github.com/emilk/egui/pull/5862) by [@wareya](https://github.com/wareya)
* Bump `ron` to `0.10.1` [#6861](https://github.com/emilk/egui/pull/6861) by [@torokati44](https://github.com/torokati44)
* Disallow `accesskit` on Android NativeActivity, making `hello_android` working again [#6855](https://github.com/emilk/egui/pull/6855) by [@podusowski](https://github.com/podusowski)
* Respect and detect `prefers-color-scheme: no-preference` [#7293](https://github.com/emilk/egui/pull/7293) by [@emilk](https://github.com/emilk)
### 🐛 Fixed
* Mark all keys as up if the app loses focus [#5743](https://github.com/emilk/egui/pull/5743) by [@emilk](https://github.com/emilk)
* Fix text input on Android [#5759](https://github.com/emilk/egui/pull/5759) by [@StratusFearMe21](https://github.com/StratusFearMe21)
* Fix text distortion on mobile devices/browsers with `glow` backend [#6893](https://github.com/emilk/egui/pull/6893) by [@wareya](https://github.com/wareya)
* Workaround libpng crash on macOS by not creating `NSImage` from png data [#7252](https://github.com/emilk/egui/pull/7252) by [@Wumpf](https://github.com/Wumpf)
* Fix incorrect window sizes for non-resizable windows on Wayland [#7103](https://github.com/emilk/egui/pull/7103) by [@GoldsteinE](https://github.com/GoldsteinE)
* Web: only consume copy/cut events if the canvas has focus [#7270](https://github.com/emilk/egui/pull/7270) by [@emilk](https://github.com/emilk)
## 0.31.1 - 2025-03-05
Nothing new

View File

@@ -24,7 +24,7 @@ To use on Linux, first run:
sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev
```
You need to either use `edition = "2021"`, or set `resolver = "2"` in the `[workspace]` section of your to-level `Cargo.toml`. See [this link](https://doc.rust-lang.org/edition-guide/rust-2021/default-cargo-resolver.html) for more info.
You need to either use `edition = "2024"`, or set `resolver = "2"` in the `[workspace]` section of your to-level `Cargo.toml`. See [this link](https://doc.rust-lang.org/edition-guide/rust-2021/default-cargo-resolver.html) for more info.
You can opt-in to the using [`egui-wgpu`](https://github.com/emilk/egui/tree/main/crates/egui-wgpu) for rendering by enabling the `wgpu` feature and setting `NativeOptions::renderer` to `Renderer::Wgpu`.

View File

@@ -574,7 +574,9 @@ impl Default for Renderer {
fn default() -> Self {
#[cfg(not(feature = "glow"))]
#[cfg(not(feature = "wgpu"))]
compile_error!("eframe: you must enable at least one of the rendering backend features: 'glow' or 'wgpu'");
compile_error!(
"eframe: you must enable at least one of the rendering backend features: 'glow' or 'wgpu'"
);
#[cfg(feature = "glow")]
#[cfg(not(feature = "wgpu"))]
@@ -617,7 +619,9 @@ impl std::str::FromStr for Renderer {
#[cfg(feature = "wgpu")]
"wgpu" => Ok(Self::Wgpu),
_ => Err(format!("eframe renderer {name:?} is not available. Make sure that the corresponding eframe feature is enabled."))
_ => Err(format!(
"eframe renderer {name:?} is not available. Make sure that the corresponding eframe feature is enabled."
)),
}
}
}

View File

@@ -205,13 +205,18 @@ fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconS
use objc2::ClassType as _;
use objc2_app_kit::{NSApplication, NSImage};
use objc2_foundation::{NSData, NSString};
use objc2_foundation::NSString;
let png_bytes = if let Some(icon_data) = icon_data {
match icon_data.to_png_bytes() {
Ok(png_bytes) => Some(png_bytes),
// Do NOT use png even though creating `NSImage` from it is much easier than from raw images data!
//
// Some MacOS versions have a bug where creating an `NSImage` from a png will cause it to load an arbitrary `libpng.dylib`.
// If this dylib isn't the right version, the application will crash with SIGBUS.
// For details see https://github.com/emilk/egui/issues/7155
let image = if let Some(icon_data) = icon_data {
match icon_data.to_image() {
Ok(image) => Some(image),
Err(err) => {
log::warn!("Failed to convert IconData to png: {err}");
log::warn!("Failed to read icon data: {err}");
return AppIconStatus::NotSetIgnored;
}
}
@@ -220,7 +225,7 @@ fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconS
};
// TODO(madsmtm): Move this into `objc2-app-kit`
extern "C" {
unsafe extern "C" {
static NSApp: Option<&'static NSApplication>;
}
@@ -231,15 +236,41 @@ fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconS
return AppIconStatus::NotSetIgnored;
};
if let Some(png_bytes) = png_bytes {
let data = NSData::from_vec(png_bytes);
if let Some(image) = image {
use objc2_app_kit::{NSBitmapImageRep, NSDeviceRGBColorSpace};
use objc2_foundation::NSSize;
log::trace!("NSImage::initWithData…");
let app_icon = NSImage::initWithData(NSImage::alloc(), &data);
log::trace!(
"NSBitmapImageRep::initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bytesPerRow_bitsPerPixel"
);
let Some(image_rep) = NSBitmapImageRep::initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bytesPerRow_bitsPerPixel(
NSBitmapImageRep::alloc(),
[image.as_raw().as_ptr().cast_mut()].as_mut_ptr(),
image.width() as isize,
image.height() as isize,
8, // bits per sample
4, // samples per pixel
true, // has alpha
false, // is not planar
NSDeviceRGBColorSpace,
(image.width() * 4) as isize, // bytes per row
32 // bits per pixel
) else {
log::warn!("Failed to create NSBitmapImageRep from app icon data.");
return AppIconStatus::NotSetIgnored;
};
log::trace!("NSImage::initWithSize");
let app_icon = NSImage::initWithSize(
NSImage::alloc(),
NSSize::new(image.width() as f64, image.height() as f64),
);
log::trace!("NSImage::addRepresentation");
app_icon.addRepresentation(&image_rep);
profiling::scope!("setApplicationIconImage_");
log::trace!("setApplicationIconImage…");
app.setApplicationIconImage(app_icon.as_deref());
app.setApplicationIconImage(Some(&app_icon));
}
// Change the title in the top bar - for python processes this would be again "python" otherwise.

View File

@@ -52,10 +52,10 @@ fn roaming_appdata() -> Option<PathBuf> {
use windows_sys::Win32::Foundation::S_OK;
use windows_sys::Win32::System::Com::CoTaskMemFree;
use windows_sys::Win32::UI::Shell::{
FOLDERID_RoamingAppData, SHGetKnownFolderPath, KF_FLAG_DONT_VERIFY,
FOLDERID_RoamingAppData, KF_FLAG_DONT_VERIFY, SHGetKnownFolderPath,
};
extern "C" {
unsafe extern "C" {
fn wcslen(buf: *const u16) -> usize;
}
let mut path_raw = ptr::null_mut();
@@ -72,7 +72,7 @@ fn roaming_appdata() -> Option<PathBuf> {
};
let path = if result == S_OK {
// SAFETY: SHGetKnownFolderPath indicated success and is supposed to allocate a nullterminated string for us.
// SAFETY: SHGetKnownFolderPath indicated success and is supposed to allocate a null-terminated string for us.
let path_slice = unsafe { slice::from_raw_parts(path_raw, wcslen(path_raw)) };
Some(PathBuf::from(OsString::from_wide(path_slice)))
} else {

View File

@@ -32,13 +32,13 @@ use egui::{
use egui_winit::accesskit_winit;
use crate::{
native::epi_integration::EpiIntegration, App, AppCreator, CreationContext, NativeOptions,
Result, Storage,
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
native::epi_integration::EpiIntegration,
};
use super::{
epi_integration, event_loop_context,
winit_integration::{create_egui_context, EventResult, UserEvent, WinitApp},
winit_integration::{EventResult, UserEvent, WinitApp, create_egui_context},
};
// ----------------------------------------------------------------------------
@@ -959,7 +959,6 @@ impl GlutinWindowContext {
.with_preference(glutin_winit::ApiPreference::FallbackEgl)
.with_window_attributes(Some(egui_winit::create_winit_window_attributes(
egui_ctx,
event_loop,
viewport_builder.clone(),
)));
@@ -1016,7 +1015,9 @@ impl GlutinWindowContext {
let gl_context = match gl_context_result {
Ok(it) => it,
Err(err) => {
log::warn!("Failed to create context using default context attributes {context_attributes:?} due to error: {err}");
log::warn!(
"Failed to create context using default context attributes {context_attributes:?} due to error: {err}"
);
log::debug!(
"Retrying with fallback context attributes: {fallback_context_attributes:?}"
);
@@ -1113,7 +1114,6 @@ impl GlutinWindowContext {
log::debug!("Creating a window for viewport {viewport_id:?}");
let window_attributes = egui_winit::create_winit_window_attributes(
&self.egui_ctx,
event_loop,
viewport.builder.clone(),
);
if window_attributes.transparent()

View File

@@ -10,9 +10,8 @@ use ahash::HashMap;
use super::winit_integration::{UserEvent, WinitApp};
use crate::{
epi,
Result, epi,
native::{event_loop_context, winit_integration::EventResult},
Result,
};
// ----------------------------------------------------------------------------

View File

@@ -25,8 +25,8 @@ use egui_winit::accesskit_winit;
use winit_integration::UserEvent;
use crate::{
native::{epi_integration::EpiIntegration, winit_integration::EventResult},
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
native::{epi_integration::EpiIntegration, winit_integration::EventResult},
};
use super::{epi_integration, event_loop_context, winit_integration, winit_integration::WinitApp};

View File

@@ -1,8 +1,8 @@
use egui::{TexturesDelta, UserData, ViewportCommand};
use crate::{epi, App};
use crate::{App, epi};
use super::{now_sec, text_agent::TextAgent, web_painter::WebPainter as _, NeedRepaint};
use super::{NeedRepaint, now_sec, text_agent::TextAgent, web_painter::WebPainter as _};
pub struct AppRunner {
#[allow(dead_code, clippy::allow_attributes)]

View File

@@ -1,11 +1,10 @@
use crate::web::string_from_js_value;
use super::{
button_from_mouse_event, location_hash, modifiers_from_kb_event, modifiers_from_mouse_event,
modifiers_from_wheel_event, native_pixels_per_point, pos_from_mouse_event,
prefers_color_scheme_dark, primary_touch_pos, push_touches, text_from_keyboard_event,
theme_from_dark_mode, translate_key, AppRunner, Closure, JsCast as _, JsValue, WebRunner,
DEBUG_RESIZE,
AppRunner, Closure, DEBUG_RESIZE, JsCast as _, JsValue, WebRunner, button_from_mouse_event,
location_hash, modifiers_from_kb_event, modifiers_from_mouse_event, modifiers_from_wheel_event,
native_pixels_per_point, pos_from_mouse_event, prefers_color_scheme, primary_touch_pos,
push_touches, text_from_keyboard_event, translate_key,
};
use web_sys::{Document, EventTarget, ShadowRoot};
@@ -311,13 +310,17 @@ pub(crate) fn on_keyup(event: web_sys::KeyboardEvent, runner: &mut AppRunner) {
fn install_copy_cut_paste(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> {
runner_ref.add_event_listener(target, "paste", |event: web_sys::ClipboardEvent, runner| {
if !runner.input.raw.focused {
return; // The eframe app is not interested
}
if let Some(data) = event.clipboard_data() {
if let Ok(text) = data.get_data("text") {
let text = text.replace("\r\n", "\n");
let mut should_stop_propagation = true;
let mut should_prevent_default = true;
if !text.is_empty() && runner.input.raw.focused {
if !text.is_empty() {
let egui_event = egui::Event::Paste(text);
should_stop_propagation =
(runner.web_options.should_stop_propagation)(&egui_event);
@@ -340,17 +343,19 @@ fn install_copy_cut_paste(runner_ref: &WebRunner, target: &EventTarget) -> Resul
})?;
runner_ref.add_event_listener(target, "cut", |event: web_sys::ClipboardEvent, runner| {
if runner.input.raw.focused {
runner.input.raw.events.push(egui::Event::Cut);
// In Safari we are only allowed to write to the clipboard during the
// event callback, which is why we run the app logic here and now:
runner.logic();
// Make sure we paint the output of the above logic call asap:
runner.needs_repaint.repaint_asap();
if !runner.input.raw.focused {
return; // The eframe app is not interested
}
runner.input.raw.events.push(egui::Event::Cut);
// In Safari we are only allowed to write to the clipboard during the
// event callback, which is why we run the app logic here and now:
runner.logic();
// Make sure we paint the output of the above logic call asap:
runner.needs_repaint.repaint_asap();
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
if (runner.web_options.should_stop_propagation)(&egui::Event::Cut) {
event.stop_propagation();
@@ -362,17 +367,19 @@ fn install_copy_cut_paste(runner_ref: &WebRunner, target: &EventTarget) -> Resul
})?;
runner_ref.add_event_listener(target, "copy", |event: web_sys::ClipboardEvent, runner| {
if runner.input.raw.focused {
runner.input.raw.events.push(egui::Event::Copy);
// In Safari we are only allowed to write to the clipboard during the
// event callback, which is why we run the app logic here and now:
runner.logic();
// Make sure we paint the output of the above logic call asap:
runner.needs_repaint.repaint_asap();
if !runner.input.raw.focused {
return; // The eframe app is not interested
}
runner.input.raw.events.push(egui::Event::Copy);
// In Safari we are only allowed to write to the clipboard during the
// event callback, which is why we run the app logic here and now:
runner.logic();
// Make sure we paint the output of the above logic call asap:
runner.needs_repaint.repaint_asap();
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
if (runner.web_options.should_stop_propagation)(&egui::Event::Copy) {
event.stop_propagation();
@@ -462,16 +469,19 @@ fn install_color_scheme_change_event(
runner_ref: &WebRunner,
window: &web_sys::Window,
) -> Result<(), JsValue> {
if let Some(media_query_list) = prefers_color_scheme_dark(window)? {
runner_ref.add_event_listener::<web_sys::MediaQueryListEvent>(
&media_query_list,
"change",
|event, runner| {
let theme = theme_from_dark_mode(event.matches());
runner.input.raw.system_theme = Some(theme);
runner.needs_repaint.repaint_asap();
},
)?;
for theme in [egui::Theme::Dark, egui::Theme::Light] {
if let Some(media_query_list) = prefers_color_scheme(window, theme)? {
runner_ref.add_event_listener::<web_sys::MediaQueryListEvent>(
&media_query_list,
"change",
|_event, runner| {
if let Some(theme) = super::system_theme() {
runner.input.raw.system_theme = Some(theme);
runner.needs_repaint.repaint_asap();
}
},
)?;
}
}
Ok(())

View File

@@ -1,4 +1,4 @@
use super::{canvas_content_rect, AppRunner};
use super::{AppRunner, canvas_content_rect};
pub fn pos_from_mouse_event(
canvas: &web_sys::HtmlCanvasElement,

View File

@@ -40,6 +40,7 @@ pub(crate) type ActiveWebPainter = web_painter_wgpu::WebPainterWgpu;
pub use backend::*;
use egui::Theme;
use wasm_bindgen::prelude::*;
use web_sys::{Document, MediaQueryList, Node};
@@ -113,24 +114,31 @@ pub fn native_pixels_per_point() -> f32 {
///
/// `None` means unknown.
pub fn system_theme() -> Option<egui::Theme> {
let dark_mode = prefers_color_scheme_dark(&web_sys::window()?)
.ok()??
.matches();
Some(theme_from_dark_mode(dark_mode))
}
fn prefers_color_scheme_dark(window: &web_sys::Window) -> Result<Option<MediaQueryList>, JsValue> {
window.match_media("(prefers-color-scheme: dark)")
}
fn theme_from_dark_mode(dark_mode: bool) -> egui::Theme {
if dark_mode {
egui::Theme::Dark
let window = web_sys::window()?;
if does_prefer_color_scheme(&window, Theme::Dark) == Some(true) {
Some(Theme::Dark)
} else if does_prefer_color_scheme(&window, Theme::Light) == Some(true) {
Some(Theme::Light)
} else {
egui::Theme::Light
None
}
}
fn does_prefer_color_scheme(window: &web_sys::Window, theme: Theme) -> Option<bool> {
Some(prefers_color_scheme(window, theme).ok()??.matches())
}
fn prefers_color_scheme(
window: &web_sys::Window,
theme: Theme,
) -> Result<Option<MediaQueryList>, JsValue> {
let theme = match theme {
Theme::Dark => "dark",
Theme::Light => "light",
};
window.match_media(format!("(prefers-color-scheme: {theme})").as_str())
}
/// Returns the canvas in client coordinates.
fn canvas_content_rect(canvas: &web_sys::HtmlCanvasElement) -> egui::Rect {
let bounding_rect = canvas.get_bounding_client_rect();

View File

@@ -126,12 +126,17 @@ fn shorten_file_path(file_path: &str) -> &str {
#[test]
fn test_shorten_file_path() {
for (before, after) in [
("/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs", "tokio-1.24.1/src/runtime/runtime.rs"),
(
"/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs",
"tokio-1.24.1/src/runtime/runtime.rs",
),
("crates/rerun/src/main.rs", "rerun/src/main.rs"),
("/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs", "core/src/ops/function.rs"),
(
"/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs",
"core/src/ops/function.rs",
),
("/weird/path/file.rs", "/weird/path/file.rs"),
]
{
] {
assert_eq!(shorten_file_path(before), after);
}
}

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
use super::web_painter::WebPainter;
use crate::WebOptions;
use egui::{Event, UserData, ViewportId};
use egui_wgpu::capture::{capture_channel, CaptureReceiver, CaptureSender, CaptureState};
use egui_wgpu::capture::{CaptureReceiver, CaptureSender, CaptureState, capture_channel};
use egui_wgpu::{RenderState, SurfaceErrorAction};
use wasm_bindgen::JsValue;
use web_sys::HtmlCanvasElement;
@@ -279,13 +279,6 @@ impl WebPainter for WebPainterWgpu {
Some((output_frame, capture_buffer))
};
{
let mut renderer = render_state.renderer.write();
for id in &textures_delta.free {
renderer.free_texture(id);
}
}
// Submit the commands: both the main buffer and user-defined ones.
render_state
.queue
@@ -307,6 +300,16 @@ impl WebPainter for WebPainterWgpu {
frame.present();
}
// 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.
{
let mut renderer = render_state.renderer.write();
for id in &textures_delta.free {
renderer.free_texture(id);
}
}
Ok(())
}

View File

@@ -2,12 +2,12 @@ use std::{cell::RefCell, rc::Rc};
use wasm_bindgen::prelude::*;
use crate::{epi, App};
use crate::{App, epi};
use super::{
AppRunner, PanicHandler,
events::{self, ResizeObserverContext},
text_agent::TextAgent,
AppRunner, PanicHandler,
};
/// This is how `eframe` runs your web application

View File

@@ -6,6 +6,12 @@ This file is updated upon each release.
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
## 0.32.0 - 2025-07-10
* Update to wgpu 25 [#6744](https://github.com/emilk/egui/pull/6744) by [@torokati44](https://github.com/torokati44)
* Free textures after submitting queue instead of before with wgpu renderer on Web [#7291](https://github.com/emilk/egui/pull/7291) by [@Wumpf](https://github.com/Wumpf)
* Improve texture filtering by doing it in gamma space [#7311](https://github.com/emilk/egui/pull/7311) by [@emilk](https://github.com/emilk)
## 0.31.1 - 2025-03-05
Nothing new

View File

@@ -1,6 +1,6 @@
use egui::{UserData, ViewportId};
use epaint::ColorImage;
use std::sync::{mpsc, Arc};
use std::sync::{Arc, mpsc};
use wgpu::{BindGroupLayout, MultisampleState, StoreOp};
/// A texture and a buffer for reading the rendered frame back to the cpu.
@@ -196,7 +196,10 @@ impl CaptureState {
wgpu::TextureFormat::Rgba8Unorm => [0, 1, 2, 3],
wgpu::TextureFormat::Bgra8Unorm => [2, 1, 0, 3],
_ => {
log::error!("Screen can't be captured unless the surface format is Rgba8Unorm or Bgra8Unorm. Current surface format is {:?}", format);
log::error!(
"Screen can't be captured unless the surface format is Rgba8Unorm or Bgra8Unorm. Current surface format is {:?}",
format
);
return;
}
};

View File

@@ -97,9 +97,8 @@ fn vs_main(
@fragment
fn fs_main_linear_framebuffer(in: VertexOutput) -> @location(0) vec4<f32> {
// We always have an sRGB aware texture at the moment.
let tex_linear = textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
let tex_gamma = gamma_from_linear_rgba(tex_linear);
// We expect "normal" textures that are NOT sRGB-aware.
let tex_gamma = textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
var out_color_gamma = in.color * tex_gamma;
// Dither the float color down to eight bits to reduce banding.
// This step is optional for egui backends.
@@ -115,9 +114,8 @@ fn fs_main_linear_framebuffer(in: VertexOutput) -> @location(0) vec4<f32> {
@fragment
fn fs_main_gamma_framebuffer(in: VertexOutput) -> @location(0) vec4<f32> {
// We always have an sRGB aware texture at the moment.
let tex_linear = textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
let tex_gamma = gamma_from_linear_rgba(tex_linear);
// We expect "normal" textures that are NOT sRGB-aware.
let tex_gamma = textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
var out_color_gamma = in.color * tex_gamma;
// Dither the float color down to eight bits to reduce banding.
// This step is optional for egui backends.

View File

@@ -3,7 +3,7 @@
use std::{borrow::Cow, num::NonZeroU64, ops::Range};
use ahash::HashMap;
use epaint::{emath::NumExt as _, PaintCallbackInfo, Primitive, Vertex};
use epaint::{PaintCallbackInfo, Primitive, Vertex, emath::NumExt as _};
use wgpu::util::DeviceExt as _;
@@ -564,15 +564,6 @@ impl Renderer {
);
Cow::Borrowed(&image.pixels)
}
epaint::ImageData::Font(image) => {
assert_eq!(
width as usize * height as usize,
image.pixels.len(),
"Mismatch between texture size and texel count"
);
profiling::scope!("font -> sRGBA");
Cow::Owned(image.srgba_pixels(None).collect::<Vec<epaint::Color32>>())
}
};
let data_bytes: &[u8] = bytemuck::cast_slice(data_color32.as_slice());
@@ -638,9 +629,9 @@ impl Renderer {
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb, // Minspec for wgpu WebGL emulation is WebGL2, so this should always be supported.
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[wgpu::TextureFormat::Rgba8UnormSrgb],
view_formats: &[wgpu::TextureFormat::Rgba8Unorm],
})
};
let origin = wgpu::Origin3d::ZERO;
@@ -699,7 +690,7 @@ impl Renderer {
///
/// This enables the application to reference the texture inside an image ui element.
/// This effectively enables off-screen rendering inside the egui UI. Texture must have
/// the texture format [`wgpu::TextureFormat::Rgba8UnormSrgb`].
/// the texture format [`wgpu::TextureFormat::Rgba8Unorm`].
pub fn register_native_texture(
&mut self,
device: &wgpu::Device,
@@ -747,7 +738,7 @@ impl Renderer {
/// This allows applications to specify individual minification/magnification filters as well as
/// custom mipmap and tiling options.
///
/// The texture must have the format [`wgpu::TextureFormat::Rgba8UnormSrgb`].
/// The texture must have the format [`wgpu::TextureFormat::Rgba8Unorm`].
/// Any compare function supplied in the [`wgpu::SamplerDescriptor`] will be ignored.
#[expect(clippy::needless_pass_by_value)] // false positive
pub fn register_native_texture_with_sampler_options(
@@ -909,7 +900,11 @@ impl Renderer {
);
let Some(mut index_buffer_staging) = index_buffer_staging else {
panic!("Failed to create staging buffer for index data. Index count: {index_count}. Required index buffer size: {required_index_buffer_size}. Actual size {} and capacity: {} (bytes)", self.index_buffer.buffer.size(), self.index_buffer.capacity);
panic!(
"Failed to create staging buffer for index data. Index count: {index_count}. Required index buffer size: {required_index_buffer_size}. Actual size {} and capacity: {} (bytes)",
self.index_buffer.buffer.size(),
self.index_buffer.capacity
);
};
let mut index_offset = 0;
@@ -948,7 +943,11 @@ impl Renderer {
);
let Some(mut vertex_buffer_staging) = vertex_buffer_staging else {
panic!("Failed to create staging buffer for vertex data. Vertex count: {vertex_count}. Required vertex buffer size: {required_vertex_buffer_size}. Actual size {} and capacity: {} (bytes)", self.vertex_buffer.buffer.size(), self.vertex_buffer.capacity);
panic!(
"Failed to create staging buffer for vertex data. Vertex count: {vertex_count}. Required vertex buffer size: {required_vertex_buffer_size}. Actual size {} and capacity: {} (bytes)",
self.vertex_buffer.buffer.size(),
self.vertex_buffer.capacity
);
};
let mut vertex_offset = 0;

View File

@@ -1,8 +1,8 @@
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::undocumented_unsafe_blocks)]
use crate::capture::{capture_channel, CaptureReceiver, CaptureSender, CaptureState};
use crate::{renderer, RenderState, SurfaceErrorAction, WgpuConfiguration};
use crate::capture::{CaptureReceiver, CaptureSender, CaptureState, capture_channel};
use crate::{RenderState, SurfaceErrorAction, WgpuConfiguration, renderer};
use egui::{Context, Event, UserData, ViewportId, ViewportIdMap, ViewportIdSet};
use std::{num::NonZeroU32, sync::Arc};
@@ -220,7 +220,9 @@ impl Painter {
} else if supported_alpha_modes.contains(&wgpu::CompositeAlphaMode::PostMultiplied) {
wgpu::CompositeAlphaMode::PostMultiplied
} else {
log::warn!("Transparent window was requested, but the active wgpu surface does not support a `CompositeAlphaMode` with transparency.");
log::warn!(
"Transparent window was requested, but the active wgpu surface does not support a `CompositeAlphaMode` with transparency."
);
wgpu::CompositeAlphaMode::Auto
}
} else {
@@ -344,7 +346,9 @@ impl Painter {
height_in_pixels,
);
} else {
log::warn!("Ignoring window resize notification with no surface created via Painter::set_window()");
log::warn!(
"Ignoring window resize notification with no surface created via Painter::set_window()"
);
}
}

View File

@@ -5,6 +5,14 @@ This file is updated upon each release.
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
## 0.32.0 - 2025-07-10
* Mark all keys as released if the app loses focus [#5743](https://github.com/emilk/egui/pull/5743) by [@emilk](https://github.com/emilk)
* Fix text input on Android [#5759](https://github.com/emilk/egui/pull/5759) by [@StratusFearMe21](https://github.com/StratusFearMe21)
* Add macOS-specific `has_shadow` and `with_has_shadow` to ViewportBuilder [#6850](https://github.com/emilk/egui/pull/6850) by [@gaelanmcmillan](https://github.com/gaelanmcmillan)
* Support for back-button on Android [#7073](https://github.com/emilk/egui/pull/7073) by [@ardocrat](https://github.com/ardocrat)
* Fix incorrect window sizes for non-resizable windows on Wayland [#7103](https://github.com/emilk/egui/pull/7103) by [@GoldsteinE](https://github.com/GoldsteinE)
## 0.31.1 - 2025-03-05
Nothing new

View File

@@ -123,7 +123,9 @@ impl Clipboard {
return;
}
log::error!("Copying images is not supported. Enable the 'clipboard' feature of `egui-winit` to enable it.");
log::error!(
"Copying images is not supported. Enable the 'clipboard' feature of `egui-winit` to enable it."
);
_ = image;
}
}

View File

@@ -1566,8 +1566,7 @@ pub fn create_window(
) -> Result<Window, winit::error::OsError> {
profiling::function_scope!();
let window_attributes =
create_winit_window_attributes(egui_ctx, event_loop, viewport_builder.clone());
let window_attributes = create_winit_window_attributes(egui_ctx, viewport_builder.clone());
let window = event_loop.create_window(window_attributes)?;
apply_viewport_builder_to_window(egui_ctx, &window, viewport_builder);
Ok(window)
@@ -1575,28 +1574,10 @@ pub fn create_window(
pub fn create_winit_window_attributes(
egui_ctx: &egui::Context,
event_loop: &ActiveEventLoop,
viewport_builder: ViewportBuilder,
) -> winit::window::WindowAttributes {
profiling::function_scope!();
// We set sizes and positions in egui:s own ui points, which depends on the egui
// zoom_factor and the native pixels per point, so we need to know that here.
// We don't know what monitor the window will appear on though, but
// we'll try to fix that after the window is created in the call to `apply_viewport_builder_to_window`.
let native_pixels_per_point = event_loop
.primary_monitor()
.or_else(|| event_loop.available_monitors().next())
.map_or_else(
|| {
log::debug!("Failed to find a monitor - assuming native_pixels_per_point of 1.0");
1.0
},
|m| m.scale_factor() as f32,
);
let zoom_factor = egui_ctx.zoom_factor();
let pixels_per_point = zoom_factor * native_pixels_per_point;
let ViewportBuilder {
title,
position,
@@ -1672,40 +1653,46 @@ pub fn create_winit_window_attributes(
})
.with_active(active.unwrap_or(true));
// Here and below: we create `LogicalSize` / `LogicalPosition` taking
// zoom factor into account. We don't have a good way to get physical size here,
// and trying to do it anyway leads to weird bugs on Wayland, see:
// https://github.com/emilk/egui/issues/7095#issuecomment-2920545377
// https://github.com/rust-windowing/winit/issues/4266
#[expect(
clippy::disallowed_types,
reason = "zoom factor is manually accounted for"
)]
#[cfg(not(target_os = "ios"))]
if let Some(size) = inner_size {
window_attributes = window_attributes.with_inner_size(PhysicalSize::new(
pixels_per_point * size.x,
pixels_per_point * size.y,
));
}
{
use winit::dpi::{LogicalPosition, LogicalSize};
let zoom_factor = egui_ctx.zoom_factor();
#[cfg(not(target_os = "ios"))]
if let Some(size) = min_inner_size {
window_attributes = window_attributes.with_min_inner_size(PhysicalSize::new(
pixels_per_point * size.x,
pixels_per_point * size.y,
));
}
if let Some(size) = inner_size {
window_attributes = window_attributes
.with_inner_size(LogicalSize::new(zoom_factor * size.x, zoom_factor * size.y));
}
#[cfg(not(target_os = "ios"))]
if let Some(size) = max_inner_size {
window_attributes = window_attributes.with_max_inner_size(PhysicalSize::new(
pixels_per_point * size.x,
pixels_per_point * size.y,
));
}
if let Some(size) = min_inner_size {
window_attributes = window_attributes
.with_min_inner_size(LogicalSize::new(zoom_factor * size.x, zoom_factor * size.y));
}
#[cfg(not(target_os = "ios"))]
if let Some(pos) = position {
window_attributes = window_attributes.with_position(PhysicalPosition::new(
pixels_per_point * pos.x,
pixels_per_point * pos.y,
));
if let Some(size) = max_inner_size {
window_attributes = window_attributes
.with_max_inner_size(LogicalSize::new(zoom_factor * size.x, zoom_factor * size.y));
}
if let Some(pos) = position {
window_attributes = window_attributes.with_position(LogicalPosition::new(
zoom_factor * pos.x,
zoom_factor * pos.y,
));
}
}
#[cfg(target_os = "ios")]
{
// Unused:
_ = egui_ctx;
_ = pixels_per_point;
_ = position;
_ = inner_size;

View File

@@ -1,6 +1,6 @@
use crate::{
emath::{remap_clamp, NumExt as _},
Id, IdMap, InputState,
emath::{NumExt as _, remap_clamp},
};
#[derive(Clone, Default)]

View File

@@ -81,7 +81,7 @@ impl<'a> Atom<'a> {
wrap_mode = Some(TextWrapMode::Truncate);
}
let (preferred, kind) = self.kind.into_sized(ui, available_size, wrap_mode);
let (intrinsic, kind) = self.kind.into_sized(ui, available_size, wrap_mode);
let size = self
.size
@@ -89,7 +89,7 @@ impl<'a> Atom<'a> {
SizedAtom {
size,
preferred_size: preferred.at_least(size),
intrinsic_size: intrinsic.at_least(self.size.unwrap_or_default()),
grow: self.grow,
kind,
}

View File

@@ -82,22 +82,9 @@ impl<'a> AtomKind<'a> {
match self {
AtomKind::Text(text) => {
let wrap_mode = wrap_mode.unwrap_or(ui.wrap_mode());
let desired_size = matches!(wrap_mode, TextWrapMode::Truncate).then(|| {
text.clone()
.into_galley(
ui,
Some(TextWrapMode::Extend),
available_size.x,
TextStyle::Button,
)
.desired_size()
});
let galley =
text.into_galley(ui, Some(wrap_mode), available_size.x, TextStyle::Button);
(
desired_size.unwrap_or_else(|| galley.desired_size()),
SizedAtomKind::Text(galley),
)
(galley.intrinsic_size(), SizedAtomKind::Text(galley))
}
AtomKind::Image(image) => {
let size = image.load_and_calc_size(ui, available_size);

View File

@@ -183,10 +183,10 @@ impl<'a> AtomLayout<'a> {
let mut desired_width = 0.0;
// Preferred width / height is the ideal size of the widget, e.g. the size where the
// intrinsic width / height is the ideal size of the widget, e.g. the size where the
// text is not wrapped. Used to set Response::intrinsic_size.
let mut preferred_width = 0.0;
let mut preferred_height = 0.0;
let mut intrinsic_width = 0.0;
let mut intrinsic_height = 0.0;
let mut height: f32 = 0.0;
@@ -203,7 +203,7 @@ impl<'a> AtomLayout<'a> {
if atoms.len() > 1 {
let gap_space = gap * (atoms.len() as f32 - 1.0);
desired_width += gap_space;
preferred_width += gap_space;
intrinsic_width += gap_space;
}
for (idx, item) in atoms.into_iter().enumerate() {
@@ -224,10 +224,10 @@ impl<'a> AtomLayout<'a> {
let size = sized.size;
desired_width += size.x;
preferred_width += sized.preferred_size.x;
intrinsic_width += sized.intrinsic_size.x;
height = height.at_least(size.y);
preferred_height = preferred_height.at_least(sized.preferred_size.y);
intrinsic_height = intrinsic_height.at_least(sized.intrinsic_size.y);
sized_items.push(sized);
}
@@ -243,10 +243,10 @@ impl<'a> AtomLayout<'a> {
let size = sized.size;
desired_width += size.x;
preferred_width += sized.preferred_size.x;
intrinsic_width += sized.intrinsic_size.x;
height = height.at_least(size.y);
preferred_height = preferred_height.at_least(sized.preferred_size.y);
intrinsic_height = intrinsic_height.at_least(sized.intrinsic_size.y);
sized_items.insert(index, sized);
}
@@ -256,7 +256,7 @@ impl<'a> AtomLayout<'a> {
let frame_size = (desired_size + margin.sum()).at_least(min_size);
let intrinsic_size =
(Vec2::new(preferred_width, preferred_height) + margin.sum()).at_least(min_size);
(Vec2::new(intrinsic_width, intrinsic_height) + margin.sum()).at_least(min_size);
let (_, rect) = ui.allocate_space(frame_size, intrinsic_size);
let mut response = ui.interact(rect, id, sense);

View File

@@ -12,8 +12,8 @@ pub struct SizedAtom<'a> {
/// size.x + gap.
pub size: Vec2,
/// Preferred size of the atom. This is used to calculate `Response::intrinsic_size`.
pub preferred_size: Vec2,
/// Intrinsic size of the atom. This is used to calculate `Response::intrinsic_size`.
pub intrinsic_size: Vec2,
pub kind: SizedAtomKind<'a>,
}

View File

@@ -204,12 +204,17 @@ fn shorten_source_file_path(path: &std::path::Path) -> String {
#[test]
fn test_shorten_path() {
for (before, after) in [
("/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs", "tokio-1.24.1/src/runtime/runtime.rs"),
(
"/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs",
"tokio-1.24.1/src/runtime/runtime.rs",
),
("crates/rerun/src/main.rs", "rerun/src/main.rs"),
("/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs", "core/src/ops/function.rs"),
(
"/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs",
"core/src/ops/function.rs",
),
("/weird/path/file.rs", "/weird/path/file.rs"),
]
{
] {
use std::str::FromStr as _;
let before = std::path::PathBuf::from_str(before).unwrap();
assert_eq!(shorten_source_file_path(&before), after);

View File

@@ -5,8 +5,8 @@
use emath::GuiRounding as _;
use crate::{
emath, pos2, Align2, Context, Id, InnerResponse, LayerId, Layout, NumExt as _, Order, Pos2,
Rect, Response, Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetRect, WidgetWithState,
Align2, Context, Id, InnerResponse, LayerId, Layout, NumExt as _, Order, Pos2, Rect, Response,
Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetRect, WidgetWithState, emath, pos2,
};
/// State of an [`Area`] that is persisted between frames.
@@ -121,6 +121,7 @@ pub struct Area {
new_pos: Option<Pos2>,
fade_in: bool,
layout: Layout,
sizing_pass: bool,
}
impl WidgetWithState for Area {
@@ -147,6 +148,7 @@ impl Area {
anchor: None,
fade_in: true,
layout: Layout::default(),
sizing_pass: false,
}
}
@@ -357,6 +359,27 @@ impl Area {
self.layout = layout;
self
}
/// While true, a sizing pass will be done. This means the area will be invisible
/// and the contents will be laid out to estimate the proper containing size of the area.
/// If false, there will be no change to the default area behavior. This is useful if the
/// area contents area dynamic and you need to need to make sure the area adjusts its size
/// accordingly.
///
/// This should only be set to true during the specific frames you want force a sizing pass.
/// Do NOT hard-code this as `.sizing_pass(true)`, as it will cause the area to never be
/// visible.
///
/// # Arguments
/// - resize: If true, the area will be resized to fit its contents. False will keep the
/// default area resizing behavior.
///
/// Default: `false`.
#[inline]
pub fn sizing_pass(mut self, resize: bool) -> Self {
self.sizing_pass = resize;
self
}
}
pub(crate) struct Prepared {
@@ -410,6 +433,7 @@ impl Area {
constrain_rect,
fade_in,
layout,
sizing_pass: force_sizing_pass,
} = self;
let constrain_rect = constrain_rect.unwrap_or_else(|| ctx.screen_rect());
@@ -425,6 +449,10 @@ impl Area {
interactable,
last_became_visible_at: None,
});
if force_sizing_pass {
sizing_pass = true;
state.size = None;
}
state.pivot = pivot;
state.interactable = interactable;
if let Some(new_pos) = new_pos {
@@ -679,7 +707,7 @@ fn automatic_area_position(ctx: &Context, layer_id: LayerId) -> Pos2 {
let current_column_bb = column_bbs.last_mut().unwrap();
if rect.left() < current_column_bb.right() {
// same column
*current_column_bb = current_column_bb.union(rect);
*current_column_bb |= rect;
} else {
// new column
column_bbs.push(rect);

View File

@@ -1,5 +1,13 @@
#[expect(unused_imports)]
use crate::{Ui, UiBuilder};
use std::sync::atomic::AtomicBool;
/// A tag to mark a container as closable.
///
/// Usually set via [`UiBuilder::closable`].
///
/// [`Ui::close`] will find the closest parent [`ClosableTag`] and set its `close` field to `true`.
/// Use [`Ui::should_close`] to check if close has been called.
#[derive(Debug, Default)]
pub struct ClosableTag {
pub close: AtomicBool,

View File

@@ -1,9 +1,9 @@
use std::hash::Hash;
use crate::{
emath, epaint, pos2, remap, remap_clamp, vec2, Context, Id, InnerResponse, NumExt as _, Rect,
Response, Sense, Stroke, TextStyle, TextWrapMode, Ui, UiBuilder, UiKind, UiStackInfo, Vec2,
WidgetInfo, WidgetText, WidgetType,
Context, Id, InnerResponse, NumExt as _, Rect, Response, Sense, Stroke, TextStyle,
TextWrapMode, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetInfo, WidgetText, WidgetType,
emath, epaint, pos2, remap, remap_clamp, vec2,
};
use emath::GuiRounding as _;
use epaint::{Shape, StrokeKind};

View File

@@ -1,9 +1,9 @@
use epaint::Shape;
use crate::{
epaint, style::StyleModifier, style::WidgetVisuals, vec2, Align2, Context, Id, InnerResponse,
NumExt as _, Painter, Popup, PopupCloseBehavior, Rect, Response, ScrollArea, Sense, Stroke,
TextStyle, TextWrapMode, Ui, UiBuilder, Vec2, WidgetInfo, WidgetText, WidgetType,
Align2, Context, Id, InnerResponse, NumExt as _, Painter, Popup, PopupCloseBehavior, Rect,
Response, ScrollArea, Sense, Stroke, TextStyle, TextWrapMode, Ui, UiBuilder, Vec2, WidgetInfo,
WidgetText, WidgetType, epaint, style::StyleModifier, style::WidgetVisuals, vec2,
};
#[expect(unused_imports)] // Documentation
@@ -293,7 +293,7 @@ impl ComboBox {
/// Check if the [`ComboBox`] with the given id has its popup menu currently opened.
pub fn is_open(ctx: &Context, id: Id) -> bool {
ctx.memory(|m| m.is_popup_open(Self::widget_to_popup_id(id)))
Popup::is_id_open(ctx, Self::widget_to_popup_id(id))
}
/// Convert a [`ComboBox`] id to the id used to store it's popup state.
@@ -315,7 +315,7 @@ fn combo_box_dyn<'c, R>(
) -> InnerResponse<Option<R>> {
let popup_id = ComboBox::widget_to_popup_id(button_id);
let is_popup_open = ui.memory(|m| m.is_popup_open(popup_id));
let is_popup_open = Popup::is_id_open(ui.ctx(), popup_id);
let wrap_mode = wrap_mode.unwrap_or_else(|| ui.wrap_mode());

View File

@@ -1,8 +1,8 @@
//! Frame container
use crate::{
epaint, layers::ShapeIdx, InnerResponse, Response, Sense, Style, Ui, UiBuilder, UiKind,
UiStackInfo,
InnerResponse, Response, Sense, Style, Ui, UiBuilder, UiKind, UiStackInfo, epaint,
layers::ShapeIdx,
};
use epaint::{Color32, CornerRadius, Margin, MarginF32, Rect, Shadow, Shape, Stroke};
@@ -143,7 +143,8 @@ pub struct Frame {
#[test]
fn frame_size() {
assert_eq!(
std::mem::size_of::<Frame>(), 32,
std::mem::size_of::<Frame>(),
32,
"Frame changed size! If it shrank - good! Update this test. If it grew - bad! Try to find a way to avoid it."
);
assert!(

View File

@@ -1,9 +1,19 @@
//! Popup menus, context menus and menu bars.
//!
//! Show menus via
//! - [`Popup::menu`] and [`Popup::context_menu`]
//! - [`Ui::menu_button`], [`MenuButton`] and [`SubMenuButton`]
//! - [`MenuBar`]
//! - [`Response::context_menu`]
//!
//! See [`MenuBar`] for an example.
use crate::style::StyleModifier;
use crate::{
Button, Color32, Context, Frame, Id, InnerResponse, IntoAtoms, Layout, Popup,
PopupCloseBehavior, Response, Style, Ui, UiBuilder, UiKind, UiStack, UiStackInfo, Widget as _,
};
use emath::{vec2, Align, RectAlign, Vec2};
use emath::{Align, RectAlign, Vec2, vec2};
use epaint::Stroke;
/// Apply a menu style to the [`Style`].
@@ -50,6 +60,7 @@ pub fn is_in_menu(ui: &Ui) -> bool {
false
}
/// Configuration and style for menus.
#[derive(Clone, Debug)]
pub struct MenuConfig {
/// Is this a menu bar?
@@ -120,8 +131,10 @@ impl MenuConfig {
}
}
/// Holds the state of the menu.
#[derive(Clone)]
pub struct MenuState {
/// The currently open sub menu in this menu.
pub open_item: Option<Id>,
last_visible_pass: u64,
}
@@ -163,13 +176,29 @@ impl MenuState {
/// The menu bar goes well in a [`crate::TopBottomPanel::top`],
/// but can also be placed in a [`crate::Window`].
/// In the latter case you may want to wrap it in [`Frame`].
///
/// ### Example:
/// ```
/// # egui::__run_test_ui(|ui| {
/// egui::MenuBar::new().ui(ui, |ui| {
/// ui.menu_button("File", |ui| {
/// if ui.button("Quit").clicked() {
/// ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close);
/// }
/// });
/// });
/// # });
/// ```
#[derive(Clone, Debug)]
pub struct Bar {
pub struct MenuBar {
config: MenuConfig,
style: StyleModifier,
}
impl Default for Bar {
#[deprecated = "Renamed to `egui::MenuBar`"]
pub type Bar = MenuBar;
impl Default for MenuBar {
fn default() -> Self {
Self {
config: MenuConfig::default(),
@@ -178,7 +207,7 @@ impl Default for Bar {
}
}
impl Bar {
impl MenuBar {
pub fn new() -> Self {
Self::default()
}
@@ -234,8 +263,8 @@ impl Bar {
/// A thin wrapper around a [`Button`] that shows a [`Popup::menu`] when clicked.
///
/// The only thing this does is search for the current menu config (if set via [`Bar`]).
/// If your menu button is not in a [`Bar`] it's fine to use [`Ui::button`] and [`Popup::menu`]
/// The only thing this does is search for the current menu config (if set via [`MenuBar`]).
/// If your menu button is not in a [`MenuBar`] it's fine to use [`Ui::button`] and [`Popup::menu`]
/// directly.
pub struct MenuButton<'a> {
pub button: Button<'a>,
@@ -341,6 +370,10 @@ impl<'a> SubMenuButton<'a> {
}
}
/// Show a submenu in a menu.
///
/// Useful if you want to make custom menu buttons.
/// Usually, just use [`MenuButton`] or [`SubMenuButton`] instead.
#[derive(Clone, Debug, Default)]
pub struct SubMenu {
config: Option<MenuConfig>,

View File

@@ -3,7 +3,7 @@
//! For instance, a [`Frame`] adds a frame and background to some contained UI.
pub(crate) mod area;
pub mod close_tag;
mod close_tag;
pub mod collapsing_header;
mod combo_box;
pub mod frame;
@@ -21,6 +21,7 @@ pub(crate) mod window;
pub use {
area::{Area, AreaState},
close_tag::ClosableTag,
collapsing_header::{CollapsingHeader, CollapsingResponse},
combo_box::*,
frame::Frame,

View File

@@ -1,7 +1,8 @@
use emath::{Align2, Vec2};
use crate::{
Area, Color32, Context, Frame, Id, InnerResponse, Order, Response, Sense, Ui, UiBuilder, UiKind,
};
use emath::{Align2, Vec2};
/// A modal dialog.
///
@@ -80,13 +81,11 @@ impl Modal {
frame,
} = self;
let (is_top_modal, any_popup_open) = ctx.memory_mut(|mem| {
let is_top_modal = ctx.memory_mut(|mem| {
mem.set_modal_layer(area.layer());
(
mem.top_modal_layer() == Some(area.layer()),
mem.any_popup_open(),
)
mem.top_modal_layer() == Some(area.layer())
});
let any_popup_open = crate::Popup::is_any_open(ctx);
let InnerResponse {
inner: (inner, backdrop_response),
response,

View File

@@ -61,7 +61,7 @@ pub fn show_tooltip_at_pointer<R>(
widget_id: Id,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
Tooltip::new(ctx.clone(), parent_layer, widget_id, PopupAnchor::Pointer)
Tooltip::always_open(ctx.clone(), parent_layer, widget_id, PopupAnchor::Pointer)
.gap(12.0)
.show(add_contents)
.map(|response| response.inner)
@@ -78,7 +78,7 @@ pub fn show_tooltip_for<R>(
widget_rect: &Rect,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
Tooltip::new(ctx.clone(), parent_layer, widget_id, *widget_rect)
Tooltip::always_open(ctx.clone(), parent_layer, widget_id, *widget_rect)
.show(add_contents)
.map(|response| response.inner)
}
@@ -94,7 +94,7 @@ pub fn show_tooltip_at<R>(
suggested_position: Pos2,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
Tooltip::new(ctx.clone(), parent_layer, widget_id, suggested_position)
Tooltip::always_open(ctx.clone(), parent_layer, widget_id, suggested_position)
.show(add_contents)
.map(|response| response.inner)
}

View File

@@ -18,8 +18,8 @@
use emath::GuiRounding as _;
use crate::{
lerp, vec2, Align, Context, CursorIcon, Frame, Id, InnerResponse, LayerId, Layout, NumExt as _,
Rangef, Rect, Sense, Stroke, Ui, UiBuilder, UiKind, UiStackInfo, Vec2,
Align, Context, CursorIcon, Frame, Id, InnerResponse, LayerId, Layout, NumExt as _, Rangef,
Rect, Sense, Stroke, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, lerp, vec2,
};
fn animate_expansion(ctx: &Context, id: Id, is_expanded: bool) -> f32 {

View File

@@ -1,11 +1,15 @@
use crate::containers::menu::{menu_style, MenuConfig, MenuState};
use crate::style::StyleModifier;
#![expect(deprecated)] // This is a new, safe wrapper around the old `Memory::popup` API.
use std::iter::once;
use emath::{Align, Pos2, Rect, RectAlign, Vec2, vec2};
use crate::{
Area, AreaState, Context, Frame, Id, InnerResponse, Key, LayerId, Layout, Order, Response,
Sense, Ui, UiKind, UiStackInfo,
containers::menu::{MenuConfig, MenuState, menu_style},
style::StyleModifier,
};
use emath::{vec2, Align, Pos2, Rect, RectAlign, Vec2};
use std::iter::once;
/// What should we anchor the popup to?
///
@@ -64,9 +68,7 @@ impl PopupAnchor {
match self {
Self::ParentRect(rect) => Some(rect),
Self::Pointer => ctx.pointer_hover_pos().map(Rect::from_pos),
Self::PointerFixed => ctx
.memory(|mem| mem.popup_position(popup_id))
.map(Rect::from_pos),
Self::PointerFixed => Popup::position_of_id(ctx, popup_id).map(Rect::from_pos),
Self::Position(pos) => Some(Rect::from_pos(pos)),
}
}
@@ -122,12 +124,12 @@ enum OpenKind<'a> {
impl OpenKind<'_> {
/// Returns `true` if the popup should be open
fn is_open(&self, id: Id, ctx: &Context) -> bool {
fn is_open(&self, popup_id: Id, ctx: &Context) -> bool {
match self {
OpenKind::Open => true,
OpenKind::Closed => false,
OpenKind::Bool(open) => **open,
OpenKind::Memory { .. } => ctx.memory(|mem| mem.is_popup_open(id)),
OpenKind::Memory { .. } => Popup::is_id_open(ctx, popup_id),
}
}
}
@@ -160,6 +162,8 @@ impl From<PopupKind> for UiKind {
}
}
/// A popup container.
#[must_use = "Call `.show()` to actually display the popup"]
pub struct Popup<'a> {
id: Id,
ctx: Context,
@@ -210,6 +214,57 @@ impl<'a> Popup<'a> {
}
}
/// Show a popup relative to some widget.
/// The popup will be always open.
///
/// See [`Self::menu`] and [`Self::context_menu`] for common use cases.
pub fn from_response(response: &Response) -> Self {
let mut popup = Self::new(
Self::default_response_id(response),
response.ctx.clone(),
response,
response.layer_id,
);
popup.widget_clicked_elsewhere = response.clicked_elsewhere();
popup
}
/// Show a popup relative to some widget,
/// toggling the open state based on the widget's click state.
///
/// See [`Self::menu`] and [`Self::context_menu`] for common use cases.
pub fn from_toggle_button_response(button_response: &Response) -> Self {
Self::from_response(button_response)
.open_memory(button_response.clicked().then_some(SetOpenCommand::Toggle))
}
/// Show a popup when the widget was clicked.
/// Sets the layout to `Layout::top_down_justified(Align::Min)`.
pub fn menu(button_response: &Response) -> Self {
Self::from_toggle_button_response(button_response)
.kind(PopupKind::Menu)
.layout(Layout::top_down_justified(Align::Min))
.style(menu_style)
.gap(0.0)
}
/// Show a context menu when the widget was secondary clicked.
/// Sets the layout to `Layout::top_down_justified(Align::Min)`.
/// In contrast to [`Self::menu`], this will open at the pointer position.
pub fn context_menu(response: &Response) -> Self {
Self::menu(response)
.open_memory(if response.secondary_clicked() {
Some(SetOpenCommand::Bool(true))
} else if response.clicked() {
// Explicitly close the menu if the widget was clicked
// Without this, the context menu would stay open if the user clicks the widget
Some(SetOpenCommand::Bool(false))
} else {
None
})
.at_pointer_fixed()
}
/// Set the kind of the popup. Used for [`Area::kind`] and [`Area::order`].
#[inline]
pub fn kind(mut self, kind: PopupKind) -> Self {
@@ -242,49 +297,6 @@ impl<'a> Popup<'a> {
self
}
/// Show a popup relative to some widget.
/// The popup will be always open.
///
/// See [`Self::menu`] and [`Self::context_menu`] for common use cases.
pub fn from_response(response: &Response) -> Self {
let mut popup = Self::new(
response.id.with("popup"),
response.ctx.clone(),
response,
response.layer_id,
);
popup.widget_clicked_elsewhere = response.clicked_elsewhere();
popup
}
/// Show a popup when the widget was clicked.
/// Sets the layout to `Layout::top_down_justified(Align::Min)`.
pub fn menu(response: &Response) -> Self {
Self::from_response(response)
.open_memory(response.clicked().then_some(SetOpenCommand::Toggle))
.kind(PopupKind::Menu)
.layout(Layout::top_down_justified(Align::Min))
.style(menu_style)
.gap(0.0)
}
/// Show a context menu when the widget was secondary clicked.
/// Sets the layout to `Layout::top_down_justified(Align::Min)`.
/// In contrast to [`Self::menu`], this will open at the pointer position.
pub fn context_menu(response: &Response) -> Self {
Self::menu(response)
.open_memory(if response.secondary_clicked() {
Some(SetOpenCommand::Bool(true))
} else if response.clicked() {
// Explicitly close the menu if the widget was clicked
// Without this, the context menu would stay open if the user clicks the widget
Some(SetOpenCommand::Bool(false))
} else {
None
})
.at_pointer_fixed()
}
/// Force the popup to be open or closed.
#[inline]
pub fn open(mut self, open: bool) -> Self {
@@ -446,7 +458,7 @@ impl<'a> Popup<'a> {
OpenKind::Open => true,
OpenKind::Closed => false,
OpenKind::Bool(open) => **open,
OpenKind::Memory { .. } => self.ctx.memory(|mem| mem.is_popup_open(self.id)),
OpenKind::Memory { .. } => Self::is_id_open(&self.ctx, self.id),
}
}
@@ -484,12 +496,43 @@ impl<'a> Popup<'a> {
self.gap,
expected_popup_size,
)
.unwrap_or_default()
}
/// Show the popup.
/// Returns `None` if the popup is not open or anchor is `PopupAnchor::Pointer` and there is
/// no pointer.
pub fn show<R>(self, content: impl FnOnce(&mut Ui) -> R) -> Option<InnerResponse<R>> {
let hover_pos = self.ctx.pointer_hover_pos();
let id = self.id;
if let OpenKind::Memory { set } = self.open_kind {
match set {
Some(SetOpenCommand::Bool(open)) => {
if open {
match self.anchor {
PopupAnchor::PointerFixed => {
self.ctx.memory_mut(|mem| mem.open_popup_at(id, hover_pos));
}
_ => Popup::open_id(&self.ctx, id),
}
} else {
Self::close_id(&self.ctx, id);
}
}
Some(SetOpenCommand::Toggle) => {
Self::toggle_id(&self.ctx, id);
}
None => {
self.ctx.memory_mut(|mem| mem.keep_popup_open(id));
}
}
}
if !self.open_kind.is_open(self.id, &self.ctx) {
return None;
}
let best_align = self.get_best_align();
let Popup {
@@ -512,34 +555,6 @@ impl<'a> Popup<'a> {
style,
} = self;
let hover_pos = ctx.pointer_hover_pos();
if let OpenKind::Memory { set, .. } = open_kind {
ctx.memory_mut(|mem| match set {
Some(SetOpenCommand::Bool(open)) => {
if open {
match self.anchor {
PopupAnchor::PointerFixed => {
mem.open_popup_at(id, hover_pos);
}
_ => mem.open_popup(id),
}
} else {
mem.close_popup(id);
}
}
Some(SetOpenCommand::Toggle) => {
mem.toggle_popup(id);
}
None => {
mem.keep_popup_open(id);
}
});
}
if !open_kind.is_open(id, &ctx) {
return None;
}
if kind != PopupKind::Tooltip {
ctx.pass_state_mut(|fs| {
fs.layers
@@ -573,10 +588,9 @@ impl<'a> Popup<'a> {
area = area.default_width(width);
}
let frame = frame.unwrap_or_else(|| Frame::popup(&ctx.style()));
let mut response = area.show(&ctx, |ui| {
style.apply(ui.style_mut());
let frame = frame.unwrap_or_else(|| Frame::popup(ui.style()));
frame.show(ui, content).inner
});
@@ -616,3 +630,65 @@ impl<'a> Popup<'a> {
Some(response)
}
}
/// ## Static methods
impl Popup<'_> {
/// The default ID when constructing a popup from the [`Response`] of e.g. a button.
pub fn default_response_id(response: &Response) -> Id {
response.id.with("popup")
}
/// Is the given popup open?
///
/// This assumes the use of either:
/// * [`Self::open_memory`]
/// * [`Self::from_toggle_button_response`]
/// * [`Self::menu`]
/// * [`Self::context_menu`]
///
/// The popup id should be the same as either you set with [`Self::id`] or the
/// default one from [`Self::default_response_id`].
pub fn is_id_open(ctx: &Context, popup_id: Id) -> bool {
ctx.memory(|mem| mem.is_popup_open(popup_id))
}
/// Is any popup open?
///
/// This assumes the egui memory is being used to track the open state of popups.
pub fn is_any_open(ctx: &Context) -> bool {
ctx.memory(|mem| mem.any_popup_open())
}
/// Open the given popup and close all others.
///
/// If you are NOT using [`Popup::show`], you must
/// also call [`crate::Memory::keep_popup_open`] as long as
/// you're showing the popup.
pub fn open_id(ctx: &Context, popup_id: Id) {
ctx.memory_mut(|mem| mem.open_popup(popup_id));
}
/// Toggle the given popup between closed and open.
///
/// Note: At most, only one popup can be open at a time.
pub fn toggle_id(ctx: &Context, popup_id: Id) {
ctx.memory_mut(|mem| mem.toggle_popup(popup_id));
}
/// Close all currently open popups.
pub fn close_all(ctx: &Context) {
ctx.memory_mut(|mem| mem.close_all_popups());
}
/// Close the given popup, if it is open.
///
/// See also [`Self::close_all`] if you want to close any / all currently open popups.
pub fn close_id(ctx: &Context, popup_id: Id) {
ctx.memory_mut(|mem| mem.close_popup(popup_id));
}
/// Get the position for this popup, if it is open.
pub fn position_of_id(ctx: &Context, popup_id: Id) -> Option<Pos2> {
ctx.memory(|mem| mem.popup_position(popup_id))
}
}

View File

@@ -1,6 +1,6 @@
use crate::{
pos2, vec2, Align2, Color32, Context, CursorIcon, Id, NumExt as _, Rect, Response, Sense,
Shape, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b,
Align2, Color32, Context, CursorIcon, Id, NumExt as _, Rect, Response, Sense, Shape, Ui,
UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b, pos2, vec2,
};
#[derive(Clone, Copy, Debug)]

View File

@@ -3,8 +3,8 @@ use core::f32;
use emath::{GuiRounding as _, Pos2};
use crate::{
emath::TSTransform, InnerResponse, LayerId, PointerButton, Rangef, Rect, Response, Sense, Ui,
UiBuilder, Vec2,
InnerResponse, LayerId, PointerButton, Rangef, Rect, Response, Sense, Ui, UiBuilder, Vec2,
emath::TSTransform,
};
/// Creates a transformation that fits a given scene rectangle into the available screen size.

View File

@@ -3,8 +3,8 @@
use std::ops::{Add, AddAssign, BitOr, BitOrAssign};
use crate::{
emath, epaint, lerp, pass_state, pos2, remap, remap_clamp, Context, CursorIcon, Id,
NumExt as _, Pos2, Rangef, Rect, Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b,
Context, CursorIcon, Id, NumExt as _, Pos2, Rangef, Rect, Sense, Ui, UiBuilder, UiKind,
UiStackInfo, Vec2, Vec2b, emath, epaint, lerp, pass_state, pos2, remap, remap_clamp,
};
#[derive(Clone, Copy, Debug)]

View File

@@ -1,4 +1,4 @@
use emath::{Align, Vec2};
use emath::{Align, NumExt as _, Vec2};
use crate::{Layout, Ui, UiBuilder};
@@ -20,8 +20,13 @@ use crate::{Layout, Ui, UiBuilder};
///
/// If the parent is not wide enough to fit all widgets, the parent will be expanded to the right.
///
/// The left widgets are first added to the ui, left-to-right.
/// Then the right widgets are added, right-to-left.
/// The left widgets are added left-to-right.
/// The right widgets are added right-to-left.
///
/// Which side is first depends on the configuration:
/// - [`Sides::extend`] - left widgets are added first
/// - [`Sides::shrink_left`] - right widgets are added first
/// - [`Sides::shrink_right`] - left widgets are added first
///
/// ```
/// # egui::__run_test_ui(|ui| {
@@ -40,6 +45,16 @@ use crate::{Layout, Ui, UiBuilder};
pub struct Sides {
height: Option<f32>,
spacing: Option<f32>,
kind: SidesKind,
wrap_mode: Option<crate::TextWrapMode>,
}
#[derive(Clone, Copy, Debug, Default)]
enum SidesKind {
#[default]
Extend,
ShrinkLeft,
ShrinkRight,
}
impl Sides {
@@ -68,63 +83,184 @@ impl Sides {
self
}
/// Try to shrink widgets on the left side.
///
/// Right widgets will be added first. The left [`Ui`]s max rect will be limited to the
/// remaining space.
#[inline]
pub fn shrink_left(mut self) -> Self {
self.kind = SidesKind::ShrinkLeft;
self
}
/// Try to shrink widgets on the right side.
///
/// Left widgets will be added first. The right [`Ui`]s max rect will be limited to the
/// remaining space.
#[inline]
pub fn shrink_right(mut self) -> Self {
self.kind = SidesKind::ShrinkRight;
self
}
/// Extend the left and right sides to fill the available space.
///
/// This is the default behavior.
/// The left widgets will be added first, followed by the right widgets.
#[inline]
pub fn extend(mut self) -> Self {
self.kind = SidesKind::Extend;
self
}
/// The text wrap mode for the shrinking side.
///
/// Does nothing if [`Self::extend`] is used (the default).
#[inline]
pub fn wrap_mode(mut self, wrap_mode: crate::TextWrapMode) -> Self {
self.wrap_mode = Some(wrap_mode);
self
}
/// Truncate the text on the shrinking side.
///
/// This is a shortcut for [`Self::wrap_mode`].
/// Does nothing if [`Self::extend`] is used (the default).
#[inline]
pub fn truncate(mut self) -> Self {
self.wrap_mode = Some(crate::TextWrapMode::Truncate);
self
}
/// Wrap the text on the shrinking side.
///
/// This is a shortcut for [`Self::wrap_mode`].
/// Does nothing if [`Self::extend`] is used (the default).
#[inline]
pub fn wrap(mut self) -> Self {
self.wrap_mode = Some(crate::TextWrapMode::Wrap);
self
}
pub fn show<RetL, RetR>(
self,
ui: &mut Ui,
add_left: impl FnOnce(&mut Ui) -> RetL,
add_right: impl FnOnce(&mut Ui) -> RetR,
) -> (RetL, RetR) {
let Self { height, spacing } = self;
let Self {
height,
spacing,
mut kind,
mut wrap_mode,
} = self;
let height = height.unwrap_or_else(|| ui.spacing().interact_size.y);
let spacing = spacing.unwrap_or_else(|| ui.spacing().item_spacing.x);
let mut top_rect = ui.available_rect_before_wrap();
top_rect.max.y = top_rect.min.y + height;
let result_left;
let result_right;
let (left_rect, left_preferred_size) = {
let left_max_rect = top_rect;
let mut left_ui = ui.new_child(
UiBuilder::new()
.max_rect(left_max_rect)
.layout(Layout::left_to_right(Align::Center)),
);
result_left = add_left(&mut left_ui);
(left_ui.min_rect(), left_ui.intrinsic_size())
};
let (right_rect, right_preferred_size) = {
let right_max_rect = top_rect.with_min_x(left_rect.max.x);
let mut right_ui = ui.new_child(
UiBuilder::new()
.max_rect(right_max_rect)
.layout(Layout::right_to_left(Align::Center)),
);
result_right = add_right(&mut right_ui);
(right_ui.min_rect(), right_ui.intrinsic_size())
};
let mut final_rect = left_rect.union(right_rect);
let min_width = left_rect.width() + spacing + right_rect.width();
if ui.is_sizing_pass() {
// Make as small as possible:
final_rect.max.x = left_rect.min.x + min_width;
} else {
// If the rects overlap, make sure we expand the allocated rect so that the parent
// ui knows we overflowed, and resizes:
final_rect.max.x = final_rect.max.x.max(left_rect.min.x + min_width);
kind = SidesKind::Extend;
wrap_mode = None;
}
let preferred_size = Vec2::new(
left_preferred_size.x + spacing + right_preferred_size.x,
left_preferred_size.y.max(right_preferred_size.y),
);
let intrinsic_size =
|left: Vec2, right: Vec2| Vec2::new(left.x + spacing + right.x, left.y.max(right.y));
ui.advance_cursor_after_rect(final_rect, preferred_size);
match kind {
SidesKind::ShrinkLeft => {
let (right_rect, right_intrinsic, result_right) = Self::create_ui(
ui,
top_rect,
Layout::right_to_left(Align::Center),
add_right,
None,
);
let available_width = top_rect.width() - right_rect.width() - spacing;
let left_rect_constraint =
top_rect.with_max_x(top_rect.min.x + available_width.at_least(0.0));
let (left_rect, left_intrinsic, result_left) = Self::create_ui(
ui,
left_rect_constraint,
Layout::left_to_right(Align::Center),
add_left,
wrap_mode,
);
(result_left, result_right)
let intrinsic = intrinsic_size(left_intrinsic, right_intrinsic);
ui.advance_cursor_after_rect(left_rect | right_rect, intrinsic);
(result_left, result_right)
}
SidesKind::ShrinkRight => {
let (left_rect, left_intrinsic, result_left) = Self::create_ui(
ui,
top_rect,
Layout::left_to_right(Align::Center),
add_left,
None,
);
let right_rect_constraint = top_rect.with_min_x(left_rect.max.x + spacing);
let (right_rect, right_intrinsic, result_right) = Self::create_ui(
ui,
right_rect_constraint,
Layout::right_to_left(Align::Center),
add_right,
wrap_mode,
);
let intrinsic_size = intrinsic_size(left_intrinsic, right_intrinsic);
ui.advance_cursor_after_rect(left_rect | right_rect, intrinsic_size);
(result_left, result_right)
}
SidesKind::Extend => {
let (left_rect, left_intrinsic, result_left) = Self::create_ui(
ui,
top_rect,
Layout::left_to_right(Align::Center),
add_left,
None,
);
let right_max_rect = top_rect.with_min_x(left_rect.max.x);
let (right_rect, right_intrinsic, result_right) = Self::create_ui(
ui,
right_max_rect,
Layout::right_to_left(Align::Center),
add_right,
None,
);
let mut final_rect = left_rect | right_rect;
let min_width = left_rect.width() + spacing + right_rect.width();
if ui.is_sizing_pass() {
final_rect.max.x = left_rect.min.x + min_width;
} else {
final_rect.max.x = final_rect.max.x.max(left_rect.min.x + min_width);
}
let intrinsic = intrinsic_size(left_intrinsic, right_intrinsic);
ui.advance_cursor_after_rect(final_rect, intrinsic);
(result_left, result_right)
}
}
}
fn create_ui<Ret>(
ui: &mut Ui,
max_rect: emath::Rect,
layout: Layout,
add_content: impl FnOnce(&mut Ui) -> Ret,
wrap_mode: Option<crate::TextWrapMode>,
) -> (emath::Rect, emath::Vec2, Ret) {
let mut child_ui = ui.new_child(UiBuilder::new().max_rect(max_rect).layout(layout));
if let Some(wrap_mode) = wrap_mode {
child_ui.style_mut().wrap_mode = Some(wrap_mode);
}
let result = add_content(&mut child_ui);
(child_ui.min_rect(), child_ui.intrinsic_size(), result)
}
}

View File

@@ -17,7 +17,25 @@ pub struct Tooltip<'a> {
impl Tooltip<'_> {
/// Show a tooltip that is always open.
#[deprecated = "Use `Tooltip::always_open` instead."]
pub fn new(
parent_widget: Id,
ctx: Context,
anchor: impl Into<PopupAnchor>,
parent_layer: LayerId,
) -> Self {
Self {
popup: Popup::new(parent_widget, ctx, anchor.into(), parent_layer)
.kind(PopupKind::Tooltip)
.gap(4.0)
.sense(Sense::hover()),
parent_layer,
parent_widget,
}
}
/// Show a tooltip that is always open.
pub fn always_open(
ctx: Context,
parent_layer: LayerId,
parent_widget: Id,
@@ -145,7 +163,7 @@ impl Tooltip<'_> {
// The popup might not be shown on at_pointer if there is no pointer.
if let Some(response) = &response {
state.tooltip_count += 1;
state.bounding_rect = state.bounding_rect.union(response.response.rect);
state.bounding_rect |= response.response.rect;
response
.response
.ctx

View File

@@ -9,7 +9,7 @@ use crate::collapsing_header::CollapsingState;
use crate::*;
use super::scroll_area::{ScrollBarVisibility, ScrollSource};
use super::{area, resize, Area, Frame, Resize, ScrollArea};
use super::{Area, Frame, Resize, ScrollArea, area, resize};
/// Builder for a floating window which can be dragged, closed, collapsed, resized and scrolled (off by default).
///

View File

@@ -4,16 +4,23 @@ use std::{borrow::Cow, cell::RefCell, panic::Location, sync::Arc, time::Duration
use emath::{GuiRounding as _, OrderedFloat};
use epaint::{
ClippedPrimitive, ClippedShape, Color32, ImageData, ImageDelta, Pos2, Rect, StrokeKind,
TessellationOptions, TextureAtlas, TextureId, Vec2,
emath::{self, TSTransform},
mutex::RwLock,
stats::PaintStats,
tessellator,
text::{FontInsert, FontPriority, Fonts},
vec2, ClippedPrimitive, ClippedShape, Color32, ImageData, ImageDelta, Pos2, Rect, StrokeKind,
TessellationOptions, TextureAtlas, TextureId, Vec2,
vec2,
};
use crate::{
Align2, CursorIcon, DeferredViewportUiCallback, FontDefinitions, Grid, Id, ImmediateViewport,
ImmediateViewportRendererCallback, Key, KeyboardShortcut, Label, LayerId, Memory,
ModifierNames, Modifiers, NumExt as _, Order, Painter, RawInput, Response, RichText,
ScrollArea, Sense, Style, TextStyle, TextureHandle, TextureOptions, Ui, ViewportBuilder,
ViewportCommand, ViewportId, ViewportIdMap, ViewportIdPair, ViewportIdSet, ViewportOutput,
Widget as _, WidgetRect, WidgetText,
animation_manager::AnimationManager,
containers::{self, area::AreaState},
data::output::PlatformOutput,
@@ -29,12 +36,6 @@ use crate::{
resize, response, scroll_area,
util::IdTypeMap,
viewport::ViewportClass,
Align2, CursorIcon, DeferredViewportUiCallback, FontDefinitions, Grid, Id, ImmediateViewport,
ImmediateViewportRendererCallback, Key, KeyboardShortcut, Label, LayerId, Memory,
ModifierNames, Modifiers, NumExt as _, Order, Painter, RawInput, Response, RichText,
ScrollArea, Sense, Style, TextStyle, TextureHandle, TextureOptions, Ui, ViewportBuilder,
ViewportCommand, ViewportId, ViewportIdMap, ViewportIdPair, ViewportIdSet, ViewportOutput,
Widget as _, WidgetRect, WidgetText,
};
#[cfg(feature = "accesskit")]
@@ -77,7 +78,7 @@ impl Default for WrappedTextureManager {
// Will be filled in later
let font_id = tex_mngr.alloc(
"egui_font_texture".into(),
epaint::FontImage::new([0, 0]).into(),
epaint::ColorImage::filled([0, 0], Color32::TRANSPARENT).into(),
Default::default(),
);
assert_eq!(
@@ -339,6 +340,15 @@ impl RepaintCause {
/// Per-viewport state related to repaint scheduling.
struct ViewportRepaintInfo {
/// Monotonically increasing counter.
///
/// Incremented at the end of [`Context::run`].
/// This can be smaller than [`Self::cumulative_pass_nr`],
/// but never larger.
cumulative_frame_nr: u64,
/// Monotonically increasing counter, counting the number of passes.
/// This can be larger than [`Self::cumulative_frame_nr`],
/// but never smaller.
cumulative_pass_nr: u64,
/// The duration which the backend will poll for new events
@@ -369,6 +379,7 @@ struct ViewportRepaintInfo {
impl Default for ViewportRepaintInfo {
fn default() -> Self {
Self {
cumulative_frame_nr: 0,
cumulative_pass_nr: 0,
// We haven't scheduled a repaint yet.
@@ -599,6 +610,8 @@ impl ContextImpl {
log::trace!("Adding new fonts");
}
let text_alpha_from_coverage = self.memory.options.style().visuals.text_alpha_from_coverage;
let mut is_new = false;
let fonts = self
@@ -613,13 +626,14 @@ impl ContextImpl {
Fonts::new(
pixels_per_point,
max_texture_side,
text_alpha_from_coverage,
self.font_definitions.clone(),
)
});
{
profiling::scope!("Fonts::begin_pass");
fonts.begin_pass(pixels_per_point, max_texture_side);
fonts.begin_pass(pixels_per_point, max_texture_side, text_alpha_from_coverage);
}
if is_new && self.memory.options.preload_font_glyphs {
@@ -850,7 +864,10 @@ impl Context {
if max_passes <= output.platform_output.num_completed_passes {
#[cfg(feature = "log")]
log::debug!("Ignoring call request_discard, because max_passes={max_passes}. Requested from {:?}", output.platform_output.request_discard_reasons);
log::debug!(
"Ignoring call request_discard, because max_passes={max_passes}. Requested from {:?}",
output.platform_output.request_discard_reasons
);
break;
}
@@ -864,6 +881,7 @@ impl Context {
} else {
viewport.num_multipass_in_row = 0;
}
viewport.repaint.cumulative_frame_nr += 1;
});
output
@@ -1134,7 +1152,7 @@ impl Context {
ID clashes happens when things like Windows or CollapsingHeaders share names,\n\
or when things like Plot and Grid:s aren't given unique id_salt:s.\n\n\
Sometimes the solution is to use ui.push_id.",
if below { "above" } else { "below" })
if below { "above" } else { "below" }),
);
}
}
@@ -1201,6 +1219,51 @@ impl Context {
self.accesskit_node_builder(w.id, |builder| res.fill_accesskit_node_common(builder));
}
#[cfg(feature = "accesskit")]
self.write(|ctx| {
use crate::{Align, pass_state::ScrollTarget, style::ScrollAnimation};
let viewport = ctx.viewport_for(ctx.viewport_id());
viewport
.input
.consume_accesskit_action_requests(res.id, |request| {
// TODO(lucasmerlin): Correctly handle the scroll unit:
// https://github.com/AccessKit/accesskit/blob/e639c0e0d8ccbfd9dff302d972fa06f9766d608e/common/src/lib.rs#L2621
const DISTANCE: f32 = 100.0;
match &request.action {
accesskit::Action::ScrollIntoView => {
viewport.this_pass.scroll_target = [
Some(ScrollTarget::new(
res.rect.x_range(),
Some(Align::Center),
ScrollAnimation::none(),
)),
Some(ScrollTarget::new(
res.rect.y_range(),
Some(Align::Center),
ScrollAnimation::none(),
)),
];
}
accesskit::Action::ScrollDown => {
viewport.this_pass.scroll_delta.0 += DISTANCE * Vec2::UP;
}
accesskit::Action::ScrollUp => {
viewport.this_pass.scroll_delta.0 += DISTANCE * Vec2::DOWN;
}
accesskit::Action::ScrollLeft => {
viewport.this_pass.scroll_delta.0 += DISTANCE * Vec2::LEFT;
}
accesskit::Action::ScrollRight => {
viewport.this_pass.scroll_delta.0 += DISTANCE * Vec2::RIGHT;
}
_ => return false,
};
true
});
});
res
}
@@ -1536,9 +1599,34 @@ impl Context {
}
}
/// The total number of completed frames.
///
/// Starts at zero, and is incremented once at the end of each call to [`Self::run`].
///
/// This is always smaller or equal to [`Self::cumulative_pass_nr`].
pub fn cumulative_frame_nr(&self) -> u64 {
self.cumulative_frame_nr_for(self.viewport_id())
}
/// The total number of completed frames.
///
/// Starts at zero, and is incremented once at the end of each call to [`Self::run`].
///
/// This is always smaller or equal to [`Self::cumulative_pass_nr_for`].
pub fn cumulative_frame_nr_for(&self, id: ViewportId) -> u64 {
self.read(|ctx| {
ctx.viewports
.get(&id)
.map_or(0, |v| v.repaint.cumulative_frame_nr)
})
}
/// The total number of completed passes (usually there is one pass per rendered frame).
///
/// Starts at zero, and is incremented for each completed pass inside of [`Self::run`] (usually once).
///
/// If you instead want to know which pass index this is within the current frame,
/// use [`Self::current_pass_index`].
pub fn cumulative_pass_nr(&self) -> u64 {
self.cumulative_pass_nr_for(self.viewport_id())
}
@@ -1554,6 +1642,18 @@ impl Context {
})
}
/// The index of the current pass in the current frame, starting at zero.
///
/// Usually this is zero, but if something called [`Self::request_discard`] to do multi-pass layout,
/// then this will be incremented for each pass.
///
/// This just reads the value of [`PlatformOutput::num_completed_passes`].
///
/// To know the total number of passes ever completed, use [`Self::cumulative_pass_nr`].
pub fn current_pass_index(&self) -> usize {
self.output(|o| o.num_completed_passes)
}
/// Call this if there is need to repaint the UI, i.e. if you are showing an animation.
///
/// If this is called at least once in a frame, then there will be another frame right after this.
@@ -1726,7 +1826,7 @@ impl Context {
/// This means the first pass will look glitchy, and ideally should not be shown to the user.
/// So [`crate::Grid`] calls [`Self::request_discard`] to cover up this glitches.
///
/// There is a limit to how many passes egui will perform, set by [`Options::max_passes`].
/// There is a limit to how many passes egui will perform, set by [`Options::max_passes`] (default=2).
/// Therefore, the request might be declined.
///
/// You can check if the current pass will be discarded with [`Self::will_discard`].
@@ -2305,7 +2405,9 @@ impl Context {
// If you see this message, it means we've been paying the cost of multi-pass for multiple frames in a row.
// This is likely a bug. `request_discard` should only be called in rare situations, when some layout changes.
let mut warning = format!("egui PERF WARNING: request_discard has been called {num_multipass_in_row} frames in a row");
let mut warning = format!(
"egui PERF WARNING: request_discard has been called {num_multipass_in_row} frames in a row"
);
self.viewport(|vp| {
for reason in &vp.output.request_discard_reasons {
warning += &format!("\n {reason}");
@@ -2587,7 +2689,7 @@ impl Context {
self.write(|ctx| {
let mut used = ctx.viewport().this_pass.used_by_panels;
for (_id, window) in ctx.memory.areas().visible_windows() {
used = used.union(window.rect());
used |= window.rect();
}
used.round_ui()
})
@@ -2983,36 +3085,54 @@ impl Context {
pub fn inspection_ui(&self, ui: &mut Ui) {
use crate::containers::CollapsingHeader;
ui.label(format!("Is using pointer: {}", self.is_using_pointer()))
.on_hover_text(
"Is egui currently using the pointer actively (e.g. dragging a slider)?",
);
ui.label(format!("Wants pointer input: {}", self.wants_pointer_input()))
.on_hover_text("Is egui currently interested in the location of the pointer (either because it is in use, or because it is hovering over a window).");
ui.label(format!(
"Wants keyboard input: {}",
self.wants_keyboard_input()
))
.on_hover_text("Is egui currently listening for text input?");
ui.label(format!(
"Keyboard focus widget: {}",
self.memory(|m| m.focused())
.as_ref()
.map(Id::short_debug_format)
.unwrap_or_default()
))
.on_hover_text("Is egui currently listening for text input?");
crate::Grid::new("egui-inspection-grid")
.num_columns(2)
.striped(true)
.show(ui, |ui| {
ui.label("Total ui frames:");
ui.monospace(ui.ctx().cumulative_frame_nr().to_string());
ui.end_row();
let pointer_pos = self
.pointer_hover_pos()
.map_or_else(String::new, |pos| format!("{pos:?}"));
ui.label(format!("Pointer pos: {pointer_pos}"));
ui.label("Total ui passes:");
ui.monospace(ui.ctx().cumulative_pass_nr().to_string());
ui.end_row();
let top_layer = self
.pointer_hover_pos()
.and_then(|pos| self.layer_id_at(pos))
.map_or_else(String::new, |layer| layer.short_debug_format());
ui.label(format!("Top layer under mouse: {top_layer}"));
ui.label("Is using pointer")
.on_hover_text("Is egui currently using the pointer actively (e.g. dragging a slider)?");
ui.monospace(self.is_using_pointer().to_string());
ui.end_row();
ui.label("Wants pointer input")
.on_hover_text("Is egui currently interested in the location of the pointer (either because it is in use, or because it is hovering over a window).");
ui.monospace(self.wants_pointer_input().to_string());
ui.end_row();
ui.label("Wants keyboard input").on_hover_text("Is egui currently listening for text input?");
ui.monospace(self.wants_keyboard_input().to_string());
ui.end_row();
ui.label("Keyboard focus widget").on_hover_text("Is egui currently listening for text input?");
ui.monospace(self.memory(|m| m.focused())
.as_ref()
.map(Id::short_debug_format)
.unwrap_or_default());
ui.end_row();
let pointer_pos = self
.pointer_hover_pos()
.map_or_else(String::new, |pos| format!("{pos:?}"));
ui.label("Pointer pos");
ui.monospace(pointer_pos);
ui.end_row();
let top_layer = self
.pointer_hover_pos()
.and_then(|pos| self.layer_id_at(pos))
.map_or_else(String::new, |layer| layer.short_debug_format());
ui.label("Top layer under mouse");
ui.monospace(top_layer);
ui.end_row();
});
ui.add_space(16.0);
@@ -3446,6 +3566,7 @@ impl Context {
/// Release all memory and textures related to the given image URI.
///
/// If you attempt to load the image again, it will be reloaded from scratch.
/// Also this cancels any ongoing loading of the image.
pub fn forget_image(&self, uri: &str) {
use load::BytesLoader as _;

View File

@@ -3,8 +3,8 @@
use epaint::ColorImage;
use crate::{
emath::{Pos2, Rect, Vec2},
Key, Theme, ViewportId, ViewportIdMap,
emath::{Pos2, Rect, Vec2},
};
/// What the integrations provides to egui at the start of each frame.

View File

@@ -6,7 +6,7 @@
//! to get callbacks on certain events ([`Context::on_begin_pass`], [`Context::on_end_pass`]).
use crate::{
text, Align, Align2, Color32, Context, FontFamily, FontId, Id, Rect, Shape, Vec2, WidgetText,
Align, Align2, Color32, Context, FontFamily, FontId, Id, Rect, Shape, Vec2, WidgetText, text,
};
/// Register this plugin on the given egui context,
@@ -102,7 +102,7 @@ impl State {
let location_rect =
Align2::RIGHT_TOP.anchor_size(pos - 4.0 * Vec2::X, location_galley.size());
painter.galley(location_rect.min, location_galley, color);
bounding_rect = bounding_rect.union(location_rect);
bounding_rect |= location_rect;
}
{
@@ -117,7 +117,7 @@ impl State {
);
let rect = Align2::LEFT_TOP.anchor_size(pos, galley.size());
painter.galley(rect.min, galley, color);
bounding_rect = bounding_rect.union(rect);
bounding_rect |= rect;
}
pos.y = bounding_rect.max.y + 4.0;

View File

@@ -1,8 +1,8 @@
use emath::GuiRounding as _;
use crate::{
vec2, Align2, Color32, Context, Id, InnerResponse, NumExt as _, Painter, Rect, Region, Style,
Ui, UiBuilder, Vec2,
Align2, Color32, Context, Id, InnerResponse, NumExt as _, Painter, Rect, Region, Style, Ui,
UiBuilder, Vec2, vec2,
};
#[cfg(debug_assertions)]

View File

@@ -2,7 +2,7 @@ use ahash::HashMap;
use emath::TSTransform;
use crate::{ahash, emath, id::IdSet, LayerId, Pos2, Rect, Sense, WidgetRect, WidgetRects};
use crate::{LayerId, Pos2, Rect, Sense, WidgetRect, WidgetRects, ahash, emath, id::IdSet};
/// Result of a hit-test against [`WidgetRects`].
///
@@ -91,6 +91,8 @@ pub fn hit_test(
}
}
close.retain(|rect| !rect.interact_rect.any_nan()); // Protect against bad input and transforms
// When using layer transforms it is common to stack layers close to each other.
// For instance, you may have a resize-separator on a panel, with two
// transform-layers on either side.
@@ -466,7 +468,7 @@ fn should_prioritize_hits_on_back(back: Rect, front: Rect) -> bool {
#[cfg(test)]
mod tests {
use emath::{pos2, vec2, Rect};
use emath::{Rect, pos2, vec2};
use crate::{Id, Sense};

View File

@@ -1,11 +1,11 @@
mod touch_state;
use crate::data::input::{
Event, EventFilter, KeyboardShortcut, Modifiers, MouseWheelUnit, PointerButton, RawInput,
TouchDeviceId, ViewportInfo, NUM_POINTER_BUTTONS,
Event, EventFilter, KeyboardShortcut, Modifiers, MouseWheelUnit, NUM_POINTER_BUTTONS,
PointerButton, RawInput, TouchDeviceId, ViewportInfo,
};
use crate::{
emath::{vec2, NumExt as _, Pos2, Rect, Vec2},
emath::{NumExt as _, Pos2, Rect, Vec2, vec2},
util::History,
};
use std::{
@@ -824,6 +824,23 @@ impl InputState {
})
}
#[cfg(feature = "accesskit")]
pub fn consume_accesskit_action_requests(
&mut self,
id: crate::Id,
mut consume: impl FnMut(&accesskit::ActionRequest) -> bool,
) {
let accesskit_id = id.accesskit_id();
self.events.retain(|event| {
if let Event::AccessKitActionRequest(request) = event {
if request.target == accesskit_id {
return !consume(request);
}
}
true
});
}
#[cfg(feature = "accesskit")]
pub fn has_accesskit_action_request(&self, id: crate::Id, action: accesskit::Action) -> bool {
self.accesskit_action_requests(id, action).next().is_some()
@@ -1448,7 +1465,10 @@ impl PointerState {
}
if let Some(pos) = self.hover_pos() {
return rect.intersects_ray(pos, self.direction());
let dir = self.direction();
if dir != Vec2::ZERO {
return rect.intersects_ray(pos, self.direction());
}
}
false
}

View File

@@ -1,9 +1,9 @@
use std::{collections::BTreeMap, fmt::Debug};
use crate::{
data::input::TouchDeviceId,
emath::{normalized_angle, Pos2, Vec2},
Event, RawInput, TouchId, TouchPhase,
data::input::TouchDeviceId,
emath::{Pos2, Vec2, normalized_angle},
};
/// All you probably need to know about a multi-touch gesture.
@@ -174,7 +174,7 @@ impl TouchState {
if added_or_removed_touches {
// Adding or removing fingers makes the average values "jump". We better forget
// about the previous values, and don't create delta information for this frame:
if let Some(ref mut state) = &mut self.gesture_state {
if let Some(state) = &mut self.gesture_state {
state.previous = None;
}
}
@@ -224,7 +224,7 @@ impl TouchState {
fn update_gesture(&mut self, time: f64, pointer_pos: Option<Pos2>) {
if let Some(dyn_state) = self.calc_dynamic_state() {
if let Some(ref mut state) = &mut self.gesture_state {
if let Some(state) = &mut self.gesture_state {
// updating an ongoing gesture
state.previous = Some(state.current);
state.current = dyn_state;

View File

@@ -1,6 +1,6 @@
//! How mouse and touch interzcts with widgets.
use crate::{hit_test, id, input_state, memory, Id, InputState, Key, WidgetRects};
use crate::{Id, InputState, Key, WidgetRects, hit_test, id, input_state, memory};
use self::{hit_test::WidgetHits, id::IdSet, input_state::PointerEvent, memory::InteractionState};

View File

@@ -1,8 +1,8 @@
//! Showing UI:s for egui/epaint types.
use crate::{
epaint, memory, pos2, remap_clamp, vec2, Color32, CursorIcon, FontFamily, FontId, Label, Mesh,
NumExt as _, Rect, Response, Sense, Shape, Slider, TextStyle, TextWrapMode, Ui, Widget,
Color32, CursorIcon, FontFamily, FontId, Label, Mesh, NumExt as _, Rect, Response, Sense,
Shape, Slider, TextStyle, TextWrapMode, Ui, Widget, epaint, memory, pos2, remap_clamp, vec2,
};
use emath::Vec2;

View File

@@ -1,8 +1,8 @@
//! Handles paint layers, i.e. how things
//! are sometimes painted behind or in front of other things.
use crate::{ahash, epaint, Id, IdMap, Rect};
use epaint::{emath::TSTransform, ClippedShape, Shape};
use crate::{Id, IdMap, Rect, ahash, epaint};
use epaint::{ClippedShape, Shape, emath::TSTransform};
/// Different layer categories
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]

View File

@@ -1,8 +1,8 @@
use emath::GuiRounding as _;
use crate::{
emath::{pos2, vec2, Align2, NumExt as _, Pos2, Rect, Vec2},
Align,
emath::{Align2, NumExt as _, Pos2, Rect, Vec2, pos2, vec2},
};
const INFINITY: f32 = f32::INFINITY;
@@ -54,8 +54,8 @@ pub(crate) struct Region {
impl Region {
/// Expand the `min_rect` and `max_rect` of this ui to include a child at the given rect.
pub fn expand_to_include_rect(&mut self, rect: Rect) {
self.min_rect = self.min_rect.union(rect);
self.max_rect = self.max_rect.union(rect);
self.min_rect |= rect;
self.max_rect |= rect;
}
/// Ensure we are big enough to contain the given X-coordinate.
@@ -738,7 +738,7 @@ impl Layout {
if self.main_wrap {
if cursor.intersects(frame_rect.shrink(1.0)) {
// make row/column larger if necessary
*cursor = cursor.union(frame_rect);
*cursor |= frame_rect;
} else {
// this is a new row or column. We temporarily use NAN for what will be filled in later.
match self.main_dir {

View File

@@ -3,7 +3,7 @@
//! Try the live web demo: <https://www.egui.rs/#demo>. Read more about egui at <https://github.com/emilk/egui>.
//!
//! `egui` is in heavy development, with each new version having breaking changes.
//! You need to have rust 1.84.0 or later to use `egui`.
//! You need to have rust 1.85.0 or later to use `egui`.
//!
//! To quickly get started with egui, you can take a look at [`eframe_template`](https://github.com/emilk/eframe_template)
//! which uses [`eframe`](https://docs.rs/eframe).
@@ -161,12 +161,10 @@
//!
//! * egui uses premultiplied alpha, so make sure your blending function is `(ONE, ONE_MINUS_SRC_ALPHA)`.
//! * Make sure your texture sampler is clamped (`GL_CLAMP_TO_EDGE`).
//! * egui prefers linear color spaces for all blending so:
//! * Use an sRGBA-aware texture if available (e.g. `GL_SRGB8_ALPHA8`).
//! * Otherwise: remember to decode gamma in the fragment shader.
//! * Decode the gamma of the incoming vertex colors in your vertex shader.
//! * Turn on sRGBA/linear framebuffer if available (`GL_FRAMEBUFFER_SRGB`).
//! * Otherwise: gamma-encode the colors before you write them again.
//! * egui prefers gamma color spaces for all blending so:
//! * Do NOT use an sRGBA-aware texture (NOT `GL_SRGB8_ALPHA8`).
//! * Multiply texture and vertex colors in gamma space
//! * Turn OFF sRGBA/gamma framebuffer (NO `GL_FRAMEBUFFER_SRGB`).
//!
//!
//! # Understanding immediate mode
@@ -463,36 +461,35 @@ pub use epaint::emath;
pub use ecolor::hex_color;
pub use ecolor::{Color32, Rgba};
pub use emath::{
lerp, pos2, remap, remap_clamp, vec2, Align, Align2, NumExt, Pos2, Rangef, Rect, RectAlign,
Vec2, Vec2b,
Align, Align2, NumExt, Pos2, Rangef, Rect, RectAlign, Vec2, Vec2b, lerp, pos2, remap,
remap_clamp, vec2,
};
pub use epaint::{
mutex,
ClippedPrimitive, ColorImage, CornerRadius, ImageData, Margin, Mesh, PaintCallback,
PaintCallbackInfo, Shadow, Shape, Stroke, StrokeKind, TextureHandle, TextureId, mutex,
text::{FontData, FontDefinitions, FontFamily, FontId, FontTweak},
textures::{TextureFilter, TextureOptions, TextureWrapMode, TexturesDelta},
ClippedPrimitive, ColorImage, CornerRadius, FontImage, ImageData, Margin, Mesh, PaintCallback,
PaintCallbackInfo, Shadow, Shape, Stroke, StrokeKind, TextureHandle, TextureId,
};
pub mod text {
pub use crate::text_selection::CCursorRange;
pub use epaint::text::{
cursor::CCursor, FontData, FontDefinitions, FontFamily, Fonts, Galley, LayoutJob,
LayoutSection, TextFormat, TextWrapping, TAB_SIZE,
FontData, FontDefinitions, FontFamily, Fonts, Galley, LayoutJob, LayoutSection, TAB_SIZE,
TextFormat, TextWrapping, cursor::CCursor,
};
}
pub use self::{
atomics::*,
containers::*,
containers::{menu::MenuBar, *},
context::{Context, RepaintCause, RequestRepaintInfo},
data::{
Key, UserData,
input::*,
output::{
self, CursorIcon, FullOutput, OpenUrl, OutputCommand, PlatformOutput,
UserAttentionType, WidgetInfo,
},
Key, UserData,
},
drag_and_drop::DragAndDrop,
epaint::text::TextWrapMode,

View File

@@ -65,7 +65,7 @@ use std::{
use ahash::HashMap;
use emath::{Float as _, OrderedFloat};
use epaint::{mutex::Mutex, textures::TextureOptions, ColorImage, TextureHandle, TextureId, Vec2};
use epaint::{ColorImage, TextureHandle, TextureId, Vec2, mutex::Mutex, textures::TextureOptions};
use crate::Context;

View File

@@ -1,6 +1,6 @@
use super::{
generate_loader_id, Bytes, BytesLoadResult, BytesLoader, BytesPoll, Context, Cow, HashMap,
LoadError, Mutex,
Bytes, BytesLoadResult, BytesLoader, BytesPoll, Context, Cow, HashMap, LoadError, Mutex,
generate_loader_id,
};
/// Maps URI:s to [`Bytes`], e.g. found with `include_bytes!`.

View File

@@ -6,8 +6,8 @@ use ahash::{HashMap, HashSet};
use epaint::emath::TSTransform;
use crate::{
area, vec2, EventFilter, Id, IdMap, LayerId, Order, Pos2, Rangef, RawInput, Rect, Style, Vec2,
ViewportId, ViewportIdMap, ViewportIdSet,
EventFilter, Id, IdMap, LayerId, Order, Pos2, Rangef, RawInput, Rect, Style, Vec2, ViewportId,
ViewportIdMap, ViewportIdSet, area, vec2,
};
mod theme;
@@ -377,8 +377,8 @@ impl Options {
reduce_texture_memory,
} = self;
use crate::containers::CollapsingHeader;
use crate::Widget as _;
use crate::containers::CollapsingHeader;
CollapsingHeader::new("⚙ Options")
.default_open(false)
@@ -408,11 +408,11 @@ impl Options {
.show(ui, |ui| {
theme_preference.radio_buttons(ui);
std::sync::Arc::make_mut(match theme {
let style = std::sync::Arc::make_mut(match theme {
Theme::Dark => dark_style,
Theme::Light => light_style,
})
.ui(ui);
});
style.ui(ui);
});
CollapsingHeader::new("✒ Painting")
@@ -1012,11 +1012,11 @@ impl OpenPopup {
}
}
/// ## Popups
/// Popups are things like combo-boxes, color pickers, menus etc.
/// Only one can be open at a time.
/// ## Deprecated popup API
/// Use [`crate::Popup`] instead.
impl Memory {
/// Is the given popup open?
#[deprecated = "Use Popup::is_id_open instead"]
pub fn is_popup_open(&self, popup_id: Id) -> bool {
self.popups
.get(&self.viewport_id)
@@ -1025,6 +1025,7 @@ impl Memory {
}
/// Is any popup open?
#[deprecated = "Use Popup::is_any_open instead"]
pub fn any_popup_open(&self) -> bool {
self.popups.contains_key(&self.viewport_id) || self.everything_is_visible()
}
@@ -1032,6 +1033,7 @@ impl Memory {
/// Open the given popup and close all others.
///
/// Note that you must call `keep_popup_open` on subsequent frames as long as the popup is open.
#[deprecated = "Use Popup::open_id instead"]
pub fn open_popup(&mut self, popup_id: Id) {
self.popups
.insert(self.viewport_id, OpenPopup::new(popup_id, None));
@@ -1042,6 +1044,7 @@ impl Memory {
/// This is needed because in some cases popups can go away without `close_popup` being
/// called. For example, when a context menu is open and the underlying widget stops
/// being rendered.
#[deprecated = "Use Popup::show instead"]
pub fn keep_popup_open(&mut self, popup_id: Id) {
if let Some(state) = self.popups.get_mut(&self.viewport_id) {
if state.id == popup_id {
@@ -1051,12 +1054,14 @@ impl Memory {
}
/// Open the popup and remember its position.
#[deprecated = "Use Popup with PopupAnchor::Position instead"]
pub fn open_popup_at(&mut self, popup_id: Id, pos: impl Into<Option<Pos2>>) {
self.popups
.insert(self.viewport_id, OpenPopup::new(popup_id, pos.into()));
}
/// Get the position for this popup.
#[deprecated = "Use Popup::position_of_id instead"]
pub fn popup_position(&self, id: Id) -> Option<Pos2> {
self.popups
.get(&self.viewport_id)
@@ -1064,6 +1069,7 @@ impl Memory {
}
/// Close any currently open popup.
#[deprecated = "Use Popup::close_all instead"]
pub fn close_all_popups(&mut self) {
self.popups.clear();
}
@@ -1071,7 +1077,9 @@ impl Memory {
/// Close the given popup, if it is open.
///
/// See also [`Self::close_all_popups`] if you want to close any / all currently open popups.
#[deprecated = "Use Popup::close_id instead"]
pub fn close_popup(&mut self, popup_id: Id) {
#[expect(deprecated)]
if self.is_popup_open(popup_id) {
self.popups.remove(&self.viewport_id);
}
@@ -1080,14 +1088,18 @@ impl Memory {
/// Toggle the given popup between closed and open.
///
/// Note: At most, only one popup can be open at a time.
#[deprecated = "Use Popup::toggle_id instead"]
pub fn toggle_popup(&mut self, popup_id: Id) {
#[expect(deprecated)]
if self.is_popup_open(popup_id) {
self.close_popup(popup_id);
} else {
self.open_popup(popup_id);
}
}
}
impl Memory {
/// If true, all windows, menus, tooltips, etc., will be visible at once.
///
/// This is useful for testing, benchmarking, pre-caching, etc.
@@ -1250,8 +1262,11 @@ impl Areas {
///
/// The two layers must have the same [`LayerId::order`].
pub fn set_sublayer(&mut self, parent: LayerId, child: LayerId) {
debug_assert_eq!(parent.order, child.order,
"DEBUG ASSERT: Trying to set sublayers across layers of different order ({:?}, {:?}), which is currently undefined behavior in egui", parent.order, child.order);
debug_assert_eq!(
parent.order, child.order,
"DEBUG ASSERT: Trying to set sublayers across layers of different order ({:?}, {:?}), which is currently undefined behavior in egui",
parent.order, child.order
);
self.sublayers.entry(parent).or_default().insert(child);

View File

@@ -30,11 +30,7 @@ impl Theme {
/// Chooses between [`Self::Dark`] or [`Self::Light`] based on a boolean value.
pub fn from_dark_mode(dark_mode: bool) -> Self {
if dark_mode {
Self::Dark
} else {
Self::Light
}
if dark_mode { Self::Dark } else { Self::Light }
}
}
@@ -93,9 +89,32 @@ impl ThemePreference {
/// Show radio-buttons to switch between light mode, dark mode and following the system theme.
pub fn radio_buttons(&mut self, ui: &mut crate::Ui) {
ui.horizontal(|ui| {
ui.selectable_value(self, Self::Light, "☀ Light");
ui.selectable_value(self, Self::Dark, "🌙 Dark");
ui.selectable_value(self, Self::System, "💻 System");
let system_theme = ui.ctx().input(|i| i.raw.system_theme);
ui.selectable_value(self, Self::System, "💻 System")
.on_hover_ui(|ui| {
ui.label("Follow the system theme preference.");
ui.add_space(4.0);
if let Some(system_theme) = system_theme {
ui.label(format!(
"The current system theme is: {}",
match system_theme {
Theme::Dark => "dark",
Theme::Light => "light",
}
));
} else {
ui.label("The system theme is unknown.");
}
});
ui.selectable_value(self, Self::Dark, "🌙 Dark")
.on_hover_text("Use the dark mode theme");
ui.selectable_value(self, Self::Light, "☀ Light")
.on_hover_text("Use the light mode theme");
});
}
}

View File

@@ -17,14 +17,13 @@
//! ```
use super::{
style::WidgetVisuals, Align, Context, Id, InnerResponse, PointerState, Pos2, Rect, Response,
Sense, TextStyle, Ui, Vec2,
Align, Context, Id, InnerResponse, PointerState, Pos2, Rect, Response, Sense, TextStyle, Ui,
Vec2, style::WidgetVisuals,
};
use crate::{
epaint, vec2,
widgets::{Button, ImageButton},
Align2, Area, Color32, Frame, Key, LayerId, Layout, NumExt as _, Order, Stroke, Style,
TextWrapMode, UiKind, WidgetText,
TextWrapMode, UiKind, WidgetText, epaint, vec2,
widgets::{Button, ImageButton},
};
use epaint::mutex::RwLock;
use std::sync::Arc;
@@ -88,7 +87,7 @@ fn set_menu_style(style: &mut Style) {
/// The menu bar goes well in a [`crate::TopBottomPanel::top`],
/// but can also be placed in a [`crate::Window`].
/// In the latter case you may want to wrap it in [`Frame`].
#[deprecated = "Use `crate::containers::menu::Bar` instead"]
#[deprecated = "Use `egui::MenuBar::new().ui(` instead"]
pub fn bar<R>(ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
ui.horizontal(|ui| {
set_menu_style(ui.style_mut());

View File

@@ -71,7 +71,8 @@ impl OperatingSystem {
#[cfg(feature = "log")]
log::warn!(
"egui: Failed to guess operating system from User-Agent {:?}. Please file an issue at https://github.com/emilk/egui/issues",
user_agent);
user_agent
);
Self::Unknown
}

View File

@@ -2,14 +2,14 @@ use std::sync::Arc;
use emath::GuiRounding as _;
use epaint::{
text::{Fonts, Galley, LayoutJob},
CircleShape, ClippedShape, CornerRadius, PathStroke, RectShape, Shape, Stroke, StrokeKind,
text::{Fonts, Galley, LayoutJob},
};
use crate::{
Color32, Context, FontId,
emath::{Align2, Pos2, Rangef, Rect, Vec2},
layers::{LayerId, PaintList, ShapeIdx},
Color32, Context, FontId,
};
/// Helper to paint shapes and text to a specific region on a specific layer.
@@ -83,6 +83,7 @@ impl Painter {
}
/// If set, colors will be modified to look like this
#[deprecated = "Use `multiply_opacity` instead"]
pub fn set_fade_to_color(&mut self, fade_to_color: Option<Color32>) {
self.fade_to_color = fade_to_color;
}

View File

@@ -1,9 +1,9 @@
use ahash::HashMap;
use crate::{id::IdSet, style, Align, Id, IdMap, LayerId, Rangef, Rect, Vec2, WidgetRects};
use crate::{Align, Id, IdMap, LayerId, Rangef, Rect, Vec2, WidgetRects, id::IdSet, style};
#[cfg(debug_assertions)]
use crate::{pos2, Align2, Color32, FontId, NumExt as _, Painter};
use crate::{Align2, Color32, FontId, NumExt as _, Painter, pos2};
/// Reset at the start of each frame.
#[derive(Clone, Debug, Default)]
@@ -318,7 +318,7 @@ impl PassState {
);
self.available_rect.min.x = panel_rect.max.x;
self.unused_rect.min.x = panel_rect.max.x;
self.used_by_panels = self.used_by_panels.union(panel_rect);
self.used_by_panels |= panel_rect;
}
/// Shrink `available_rect`.
@@ -329,7 +329,7 @@ impl PassState {
);
self.available_rect.max.x = panel_rect.min.x;
self.unused_rect.max.x = panel_rect.min.x;
self.used_by_panels = self.used_by_panels.union(panel_rect);
self.used_by_panels |= panel_rect;
}
/// Shrink `available_rect`.
@@ -340,7 +340,7 @@ impl PassState {
);
self.available_rect.min.y = panel_rect.max.y;
self.unused_rect.min.y = panel_rect.max.y;
self.used_by_panels = self.used_by_panels.union(panel_rect);
self.used_by_panels |= panel_rect;
}
/// Shrink `available_rect`.
@@ -351,13 +351,13 @@ impl PassState {
);
self.available_rect.max.y = panel_rect.min.y;
self.unused_rect.max.y = panel_rect.min.y;
self.used_by_panels = self.used_by_panels.union(panel_rect);
self.used_by_panels |= panel_rect;
}
pub(crate) fn allocate_central_panel(&mut self, panel_rect: Rect) {
// Note: we do not shrink `available_rect`, because
// we allow windows to cover the CentralPanel.
self.unused_rect = Rect::NOTHING; // Nothing left unused after this
self.used_by_panels = self.used_by_panels.union(panel_rect);
self.used_by_panels |= panel_rect;
}
}

View File

@@ -258,7 +258,7 @@ impl Placer {
let region = &mut self.region;
region.max_rect.min.x = rect.min.x;
region.max_rect.max.x = rect.max.x;
region.max_rect = region.max_rect.union(region.min_rect); // make sure we didn't shrink too much
region.max_rect |= region.min_rect; // make sure we didn't shrink too much
region.cursor.min.x = region.max_rect.min.x;
region.cursor.max.x = region.max_rect.max.x;
@@ -275,7 +275,7 @@ impl Placer {
let region = &mut self.region;
region.max_rect.min.y = rect.min.y;
region.max_rect.max.y = rect.max.y;
region.max_rect = region.max_rect.union(region.min_rect); // make sure we didn't shrink too much
region.max_rect |= region.min_rect; // make sure we didn't shrink too much
region.cursor.min.y = region.max_rect.min.y;
region.cursor.max.y = region.max_rect.max.y;

View File

@@ -1,9 +1,10 @@
use std::{any::Any, sync::Arc};
use crate::{
Context, CursorIcon, Id, LayerId, PointerButton, Popup, PopupKind, Sense, Tooltip, Ui,
WidgetRect, WidgetText,
emath::{Align, Pos2, Rect, Vec2},
pass_state, Context, CursorIcon, Id, LayerId, PointerButton, Popup, PopupKind, Sense, Tooltip,
Ui, WidgetRect, WidgetText,
pass_state,
};
// ----------------------------------------------------------------------------
@@ -58,8 +59,8 @@ pub struct Response {
/// The intrinsic / desired size of the widget.
///
/// For a button, this will be the size of the label + the frames padding,
/// even if the button is laid out in a justified layout and the actual size will be larger.
/// 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.
///

View File

@@ -3,14 +3,15 @@
#![allow(clippy::if_same_then_else)]
use emath::Align;
use epaint::{text::FontTweak, CornerRadius, Shadow, Stroke};
use epaint::{AlphaFromCoverage, CornerRadius, Shadow, Stroke, text::FontTweak};
use std::{collections::BTreeMap, ops::RangeInclusive, sync::Arc};
use crate::{
ecolor::Color32,
emath::{pos2, vec2, Rangef, Rect, Vec2},
ComboBox, CursorIcon, FontFamily, FontId, Grid, Margin, Response, RichText, TextWrapMode,
WidgetText,
ecolor::Color32,
emath::{Rangef, Rect, Vec2, pos2, vec2},
reset_button_with,
};
/// How to format numbers in e.g. a [`crate::DragValue`].
@@ -920,6 +921,9 @@ pub struct Visuals {
/// this is more to provide a convenient summary of the rest of the settings.
pub dark_mode: bool,
/// ADVANCED: Controls how we render text.
pub text_alpha_from_coverage: AlphaFromCoverage,
/// Override default text color for all text.
///
/// This is great for setting the color of text for any widget.
@@ -935,6 +939,17 @@ pub struct Visuals {
/// it is disabled, non-interactive, hovered etc.
pub override_text_color: Option<Color32>,
/// How strong "weak" text is.
///
/// Ignored if [`Self::weak_text_color`] is set.
pub weak_text_alpha: f32,
/// Color of "weak" text.
///
/// If `None`, the color is [`Self::text_color`]
/// multiplied by [`Self::weak_text_alpha`].
pub weak_text_color: Option<Color32>,
/// Visual styles of widgets
pub widgets: Widgets,
@@ -952,6 +967,11 @@ pub struct Visuals {
/// that needs to look different from other interactive stuff.
pub extreme_bg_color: Color32,
/// The background color of [`crate::TextEdit`].
///
/// Defaults to [`Self::extreme_bg_color`].
pub text_edit_bg_color: Option<Color32>,
/// Background color behind code-styled monospaced labels.
pub code_bg_color: Color32,
@@ -1019,6 +1039,9 @@ pub struct Visuals {
/// How to display numeric color values.
pub numeric_color_space: NumericColorSpace,
/// How much to modify the alpha of a disabled widget.
pub disabled_alpha: f32,
}
impl Visuals {
@@ -1034,7 +1057,8 @@ impl Visuals {
}
pub fn weak_text_color(&self) -> Color32 {
self.gray_out(self.text_color())
self.weak_text_color
.unwrap_or_else(|| self.text_color().gamma_multiply(self.weak_text_alpha))
}
#[inline(always)]
@@ -1042,6 +1066,11 @@ impl Visuals {
self.widgets.active.text_color()
}
/// The background color of [`crate::TextEdit`].
pub fn text_edit_bg_color(&self) -> Color32 {
self.text_edit_bg_color.unwrap_or(self.extreme_bg_color)
}
/// Window background color.
#[inline(always)]
pub fn window_fill(&self) -> Color32 {
@@ -1054,17 +1083,32 @@ impl Visuals {
}
/// When fading out things, we fade the colors towards this.
// TODO(emilk): replace with an alpha
#[inline(always)]
#[deprecated = "Use disabled_alpha(). Fading is now handled by modifying the alpha channel."]
pub fn fade_out_to_color(&self) -> Color32 {
self.widgets.noninteractive.weak_bg_fill
}
/// Returned a "grayed out" version of the given color.
/// Disabled widgets have their alpha modified by this.
#[inline(always)]
pub fn disabled_alpha(&self) -> f32 {
self.disabled_alpha
}
/// Returns a "disabled" version of the given color.
///
/// This function modifies the opcacity of the given color.
/// If this is undesirable use [`gray_out`](Self::gray_out).
#[inline(always)]
pub fn disable(&self, color: Color32) -> Color32 {
color.gamma_multiply(self.disabled_alpha())
}
/// Returns a "grayed out" version of the given color.
#[doc(alias = "grey_out")]
#[inline(always)]
pub fn gray_out(&self, color: Color32) -> Color32 {
crate::ecolor::tint_color_towards(color, self.fade_out_to_color())
crate::ecolor::tint_color_towards(color, self.widgets.noninteractive.weak_bg_fill)
}
}
@@ -1333,12 +1377,16 @@ impl Visuals {
pub fn dark() -> Self {
Self {
dark_mode: true,
text_alpha_from_coverage: AlphaFromCoverage::DARK_MODE_DEFAULT,
override_text_color: None,
weak_text_alpha: 0.6,
weak_text_color: None,
widgets: Widgets::default(),
selection: Selection::default(),
hyperlink_color: Color32::from_rgb(90, 170, 255),
faint_bg_color: Color32::from_additive_luminance(5), // visible, but barely so
extreme_bg_color: Color32::from_gray(10), // e.g. TextEdit background
text_edit_bg_color: None, // use `extreme_bg_color` by default
code_bg_color: Color32::from_gray(64),
warn_fg_color: Color32::from_rgb(255, 143, 0), // orange
error_fg_color: Color32::from_rgb(255, 0, 0), // red
@@ -1384,6 +1432,7 @@ impl Visuals {
image_loading_spinners: true,
numeric_color_space: NumericColorSpace::GammaByte,
disabled_alpha: 0.5,
}
}
@@ -1391,6 +1440,7 @@ impl Visuals {
pub fn light() -> Self {
Self {
dark_mode: false,
text_alpha_from_coverage: AlphaFromCoverage::LIGHT_MODE_DEFAULT,
widgets: Widgets::light(),
selection: Selection::light(),
hyperlink_color: Color32::from_rgb(0, 155, 255),
@@ -1557,8 +1607,8 @@ impl Default for Widgets {
// ----------------------------------------------------------------------------
use crate::{
widgets::{reset_button, DragValue, Slider, Widget},
Ui,
widgets::{DragValue, Slider, Widget, reset_button},
};
impl Style {
@@ -1680,11 +1730,11 @@ impl Style {
ui.end_row();
});
ui.collapsing("🔠 Text Styles", |ui| text_styles_ui(ui, text_styles));
ui.collapsing("🔠 Text styles", |ui| text_styles_ui(ui, text_styles));
ui.collapsing("📏 Spacing", |ui| spacing.ui(ui));
ui.collapsing("☝ Interaction", |ui| interaction.ui(ui));
ui.collapsing("🎨 Visuals", |ui| visuals.ui(ui));
ui.collapsing("🔄 Scroll Animation", |ui| scroll_animation.ui(ui));
ui.collapsing("🔄 Scroll animation", |ui| scroll_animation.ui(ui));
#[cfg(debug_assertions)]
ui.collapsing("🐛 Debug", |ui| debug.ui(ui));
@@ -2022,13 +2072,17 @@ impl WidgetVisuals {
impl Visuals {
pub fn ui(&mut self, ui: &mut crate::Ui) {
let Self {
dark_mode: _,
dark_mode,
text_alpha_from_coverage,
override_text_color: _,
weak_text_alpha,
weak_text_color,
widgets,
selection,
hyperlink_color,
faint_bg_color,
extreme_bg_color,
text_edit_bg_color,
code_bg_color,
warn_fg_color,
error_fg_color,
@@ -2063,44 +2117,115 @@ impl Visuals {
image_loading_spinners,
numeric_color_space,
disabled_alpha,
} = self;
ui.collapsing("Background Colors", |ui| {
ui_color(ui, &mut widgets.inactive.weak_bg_fill, "Buttons");
ui_color(ui, window_fill, "Windows");
ui_color(ui, panel_fill, "Panels");
ui_color(ui, faint_bg_color, "Faint accent").on_hover_text(
"Used for faint accentuation of interactive things, like striped grids.",
);
ui_color(ui, extreme_bg_color, "Extreme")
.on_hover_text("Background of plots and paintings");
fn ui_optional_color(
ui: &mut Ui,
color: &mut Option<Color32>,
default_value: Color32,
label: impl Into<WidgetText>,
) -> Response {
let label_response = ui.label(label);
ui.horizontal(|ui| {
let mut set = color.is_some();
ui.checkbox(&mut set, "");
if set {
let color = color.get_or_insert(default_value);
ui.color_edit_button_srgba(color);
} else {
*color = None;
};
});
ui.end_row();
label_response
}
ui.collapsing("Background colors", |ui| {
Grid::new("background_colors")
.num_columns(2)
.show(ui, |ui| {
fn ui_color(
ui: &mut Ui,
color: &mut Color32,
label: impl Into<WidgetText>,
) -> Response {
let label_response = ui.label(label);
ui.color_edit_button_srgba(color);
ui.end_row();
label_response
}
ui_color(ui, &mut widgets.inactive.weak_bg_fill, "Buttons");
ui_color(ui, window_fill, "Windows");
ui_color(ui, panel_fill, "Panels");
ui_color(ui, faint_bg_color, "Faint accent").on_hover_text(
"Used for faint accentuation of interactive things, like striped grids.",
);
ui_color(ui, extreme_bg_color, "Extreme")
.on_hover_text("Background of plots and paintings");
ui_optional_color(ui, text_edit_bg_color, *extreme_bg_color, "TextEdit")
.on_hover_text("Background of TextEdit");
});
});
ui.collapsing("Text color", |ui| {
ui_text_color(ui, &mut widgets.noninteractive.fg_stroke.color, "Label");
ui_text_color(
ui,
&mut widgets.inactive.fg_stroke.color,
"Unhovered button",
);
ui_text_color(ui, &mut widgets.hovered.fg_stroke.color, "Hovered button");
ui_text_color(ui, &mut widgets.active.fg_stroke.color, "Clicked button");
fn ui_text_color(ui: &mut Ui, color: &mut Color32, label: impl Into<RichText>) {
ui.label(label.into().color(*color));
ui.color_edit_button_srgba(color);
ui.end_row();
}
ui_text_color(ui, warn_fg_color, RichText::new("Warnings"));
ui_text_color(ui, error_fg_color, RichText::new("Errors"));
Grid::new("text_color").num_columns(2).show(ui, |ui| {
ui_text_color(ui, &mut widgets.noninteractive.fg_stroke.color, "Label");
ui_text_color(ui, hyperlink_color, "hyperlink_color");
ui_text_color(
ui,
&mut widgets.inactive.fg_stroke.color,
"Unhovered button",
);
ui_text_color(ui, &mut widgets.hovered.fg_stroke.color, "Hovered button");
ui_text_color(ui, &mut widgets.active.fg_stroke.color, "Clicked button");
ui_color(ui, code_bg_color, RichText::new("Code background").code()).on_hover_ui(
|ui| {
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
ui.label("For monospaced inlined text ");
ui.code("like this");
ui.label(".");
ui_text_color(ui, warn_fg_color, RichText::new("Warnings"));
ui_text_color(ui, error_fg_color, RichText::new("Errors"));
ui_text_color(ui, hyperlink_color, "hyperlink_color");
ui.label(RichText::new("Code background").code())
.on_hover_ui(|ui| {
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
ui.label("For monospaced inlined text ");
ui.code("like this");
ui.label(".");
});
});
},
);
ui.color_edit_button_srgba(code_bg_color);
ui.end_row();
ui.label("Weak text alpha");
ui.add_enabled(
weak_text_color.is_none(),
DragValue::new(weak_text_alpha).speed(0.01).range(0.0..=1.0),
);
ui.end_row();
ui_optional_color(
ui,
weak_text_color,
widgets.noninteractive.text_color(),
"Weak text color",
);
});
ui.add_space(4.0);
text_alpha_from_coverage_ui(ui, text_alpha_from_coverage);
});
ui.collapsing("Text cursor", |ui| {
@@ -2191,12 +2316,60 @@ impl Visuals {
ui.label("Color picker type");
numeric_color_space.toggle_button_ui(ui);
});
ui.add(Slider::new(disabled_alpha, 0.0..=1.0).text("Disabled element alpha"));
});
ui.vertical_centered(|ui| reset_button(ui, self, "Reset visuals"));
let dark_mode = *dark_mode;
ui.vertical_centered(|ui| {
reset_button_with(
ui,
self,
"Reset visuals",
if dark_mode {
Self::dark()
} else {
Self::light()
},
);
});
}
}
fn text_alpha_from_coverage_ui(ui: &mut Ui, text_alpha_from_coverage: &mut AlphaFromCoverage) {
let mut dark_mode_special =
*text_alpha_from_coverage == AlphaFromCoverage::TwoCoverageMinusCoverageSq;
ui.horizontal(|ui| {
ui.label("Text rendering:");
ui.checkbox(&mut dark_mode_special, "Dark-mode special");
if dark_mode_special {
*text_alpha_from_coverage = AlphaFromCoverage::TwoCoverageMinusCoverageSq;
} else {
let mut gamma = match text_alpha_from_coverage {
AlphaFromCoverage::Linear => 1.0,
AlphaFromCoverage::Gamma(gamma) => *gamma,
AlphaFromCoverage::TwoCoverageMinusCoverageSq => 0.5, // approximately the same
};
ui.add(
DragValue::new(&mut gamma)
.speed(0.01)
.range(0.1..=4.0)
.prefix("Gamma: "),
);
if gamma == 1.0 {
*text_alpha_from_coverage = AlphaFromCoverage::Linear;
} else {
*text_alpha_from_coverage = AlphaFromCoverage::Gamma(gamma);
}
}
});
}
impl TextCursorStyle {
fn ui(&mut self, ui: &mut Ui) {
let Self {
@@ -2310,22 +2483,6 @@ fn two_drag_values(value: &mut Vec2, range: std::ops::RangeInclusive<f32>) -> im
}
}
fn ui_color(ui: &mut Ui, color: &mut Color32, label: impl Into<WidgetText>) -> Response {
ui.horizontal(|ui| {
ui.color_edit_button_srgba(color);
ui.label(label);
})
.response
}
fn ui_text_color(ui: &mut Ui, color: &mut Color32, label: impl Into<RichText>) -> Response {
ui.horizontal(|ui| {
ui.color_edit_button_srgba(color);
ui.label(label.into().color(*color));
})
.response
}
impl HandleShape {
pub fn ui(&mut self, ui: &mut Ui) {
ui.horizontal(|ui| {

View File

@@ -2,7 +2,7 @@ use emath::TSTransform;
use crate::{Context, Galley, Id};
use super::{text_cursor_state::is_word_char, CCursorRange};
use super::{CCursorRange, text_cursor_state::is_word_char};
/// Update accesskit with the current text state.
pub fn update_accesskit_for_text_widget(

View File

@@ -1,6 +1,6 @@
use epaint::{text::cursor::CCursor, Galley};
use epaint::{Galley, text::cursor::CCursor};
use crate::{os::OperatingSystem, Event, Id, Key, Modifiers};
use crate::{Event, Id, Key, Modifiers, os::OperatingSystem};
use super::text_cursor_state::{ccursor_next_word, ccursor_previous_word, slice_char_range};

View File

@@ -3,14 +3,14 @@ use std::sync::Arc;
use emath::TSTransform;
use crate::{
layers::ShapeIdx, text::CCursor, text_selection::CCursorRange, Context, CursorIcon, Event,
Galley, Id, LayerId, Pos2, Rect, Response, Ui,
Context, CursorIcon, Event, Galley, Id, LayerId, Pos2, Rect, Response, Ui, layers::ShapeIdx,
text::CCursor, text_selection::CCursorRange,
};
use super::{
text_cursor_state::cursor_rect,
visuals::{paint_text_selection, RowVertexIndices},
TextCursorState,
text_cursor_state::cursor_rect,
visuals::{RowVertexIndices, paint_text_selection},
};
/// Turn on to help debug this
@@ -546,7 +546,7 @@ impl LabelSelectionState {
if let Some(mut cursor_range) = cursor_state.range(galley) {
let galley_rect = global_from_galley * Rect::from_min_size(Pos2::ZERO, galley.size());
self.selection_bbox_this_frame = self.selection_bbox_this_frame.union(galley_rect);
self.selection_bbox_this_frame |= galley_rect;
if let Some(selection) = &self.selection {
if selection.primary.widget_id == response.id {

View File

@@ -1,9 +1,9 @@
//! Text cursor changes/interaction, without modifying the text.
use epaint::text::{cursor::CCursor, Galley};
use epaint::text::{Galley, cursor::CCursor};
use unicode_segmentation::UnicodeSegmentation as _;
use crate::{epaint, NumExt as _, Rect, Response, Ui};
use crate::{NumExt as _, Rect, Response, Ui, epaint};
use super::CCursorRange;

View File

@@ -1,6 +1,6 @@
use std::sync::Arc;
use crate::{pos2, vec2, Galley, Painter, Rect, Ui, Visuals};
use crate::{Galley, Painter, Rect, Ui, Visuals, pos2, vec2};
use super::CCursorRange;

View File

@@ -5,11 +5,15 @@ use emath::GuiRounding as _;
use epaint::mutex::RwLock;
use std::{any::Any, hash::Hash, sync::Arc};
use crate::close_tag::ClosableTag;
use crate::containers::menu;
use crate::ClosableTag;
#[cfg(debug_assertions)]
use crate::Stroke;
use crate::containers::menu;
use crate::{
Align, Color32, Context, CursorIcon, DragAndDrop, Id, InnerResponse, InputState, IntoAtoms,
LayerId, Memory, Order, Painter, PlatformOutput, Pos2, Rangef, Rect, Response, Rgba, RichText,
Sense, Style, TextStyle, TextWrapMode, UiBuilder, UiKind, UiStack, UiStackInfo, Vec2,
WidgetRect, WidgetText,
containers::{CollapsingHeader, CollapsingResponse, Frame},
ecolor::Hsva,
emath, epaint,
@@ -22,13 +26,9 @@ use crate::{
util::IdTypeMap,
vec2, widgets,
widgets::{
color_picker, Button, Checkbox, DragValue, Hyperlink, Image, ImageSource, Label, Link,
RadioButton, SelectableLabel, Separator, Spinner, TextEdit, Widget,
Button, Checkbox, DragValue, Hyperlink, Image, ImageSource, Label, Link, RadioButton,
Separator, Spinner, TextEdit, Widget, color_picker,
},
Align, Color32, Context, CursorIcon, DragAndDrop, Id, InnerResponse, InputState, IntoAtoms,
LayerId, Memory, Order, Painter, PlatformOutput, Pos2, Rangef, Rect, Response, Rgba, RichText,
Sense, Style, TextStyle, TextWrapMode, UiBuilder, UiKind, UiStack, UiStackInfo, Vec2,
WidgetRect, WidgetText,
};
// ----------------------------------------------------------------------------
@@ -522,7 +522,7 @@ impl Ui {
self.enabled = false;
if self.is_visible() {
self.painter
.set_fade_to_color(Some(self.visuals().fade_out_to_color()));
.multiply_opacity(self.visuals().disabled_alpha());
}
}
@@ -2101,13 +2101,13 @@ impl Ui {
Checkbox::new(checked, atoms).ui(self)
}
/// Acts like a checkbox, but looks like a [`SelectableLabel`].
/// Acts like a checkbox, but looks like a [`Button::selectable`].
///
/// Click to toggle to bool.
///
/// See also [`Self::checkbox`].
pub fn toggle_value(&mut self, selected: &mut bool, text: impl Into<WidgetText>) -> Response {
let mut response = self.selectable_label(*selected, text);
pub fn toggle_value<'a>(&mut self, selected: &mut bool, atoms: impl IntoAtoms<'a>) -> Response {
let mut response = self.selectable_label(*selected, atoms);
if response.clicked() {
*selected = !*selected;
response.mark_changed();
@@ -2158,10 +2158,10 @@ impl Ui {
/// Show a label which can be selected or not.
///
/// See also [`SelectableLabel`] and [`Self::toggle_value`].
/// See also [`Button::selectable`] and [`Self::toggle_value`].
#[must_use = "You should check if the user clicked this with `if ui.selectable_label(…).clicked() { … } "]
pub fn selectable_label(&mut self, checked: bool, text: impl Into<WidgetText>) -> Response {
SelectableLabel::new(checked, text).ui(self)
pub fn selectable_label<'a>(&mut self, checked: bool, text: impl IntoAtoms<'a>) -> Response {
Button::selectable(checked, text).ui(self)
}
/// Show selectable text. It is selected if `*current_value == selected_value`.
@@ -2169,12 +2169,12 @@ impl Ui {
///
/// Example: `ui.selectable_value(&mut my_enum, Enum::Alternative, "Alternative")`.
///
/// See also [`SelectableLabel`] and [`Self::toggle_value`].
pub fn selectable_value<Value: PartialEq>(
/// See also [`Button::selectable`] and [`Self::toggle_value`].
pub fn selectable_value<'a, Value: PartialEq>(
&mut self,
current_value: &mut Value,
selected_value: Value,
text: impl Into<WidgetText>,
text: impl IntoAtoms<'a>,
) -> Response {
let mut response = self.selectable_label(*current_value == selected_value, text);
if response.clicked() && *current_value != selected_value {
@@ -2998,8 +2998,8 @@ impl Ui {
if is_anything_being_dragged && !can_accept_what_is_being_dragged {
// When dragging something else, show that it can't be dropped here:
fill = self.visuals().gray_out(fill);
stroke.color = self.visuals().gray_out(stroke.color);
fill = self.visuals().disable(fill);
stroke.color = self.visuals().disable(stroke.color);
}
frame.frame.fill = fill;

View File

@@ -1,6 +1,6 @@
use std::{hash::Hash, sync::Arc};
use crate::close_tag::ClosableTag;
use crate::ClosableTag;
#[expect(unused_imports)] // Used for doclinks
use crate::Ui;
use crate::{Id, LayerId, Layout, Rect, Sense, Style, UiStackInfo};

View File

@@ -438,7 +438,7 @@ impl ViewportBuilder {
}
/// macOS: Set to `true` to allow the window to be moved by dragging the background.
/// Enabling this feature can result in unexpected behaviour with draggable UI widgets such as sliders.
/// Enabling this feature can result in unexpected behavior with draggable UI widgets such as sliders.
#[inline]
pub fn with_movable_by_background(mut self, value: bool) -> Self {
self.movable_by_window_background = Some(value);

View File

@@ -4,8 +4,8 @@ use std::fmt::Formatter;
use std::{borrow::Cow, sync::Arc};
use crate::{
text::{LayoutJob, TextWrapping},
Align, Color32, FontFamily, FontSelection, Galley, Style, TextStyle, TextWrapMode, Ui, Visuals,
text::{LayoutJob, TextWrapping},
};
/// Text and optional style choices for it.

View File

@@ -29,6 +29,7 @@ pub struct Button<'a> {
stroke: Option<Stroke>,
small: bool,
frame: Option<bool>,
frame_when_inactive: bool,
min_size: Vec2,
corner_radius: Option<CornerRadius>,
selected: bool,
@@ -44,6 +45,7 @@ impl<'a> Button<'a> {
stroke: None,
small: false,
frame: None,
frame_when_inactive: true,
min_size: Vec2::ZERO,
corner_radius: None,
selected: false,
@@ -52,6 +54,27 @@ impl<'a> Button<'a> {
}
}
/// Show a selectable button.
///
/// Equivalent to:
/// ```rust
/// # use egui::{Button, IntoAtoms, __run_test_ui};
/// # __run_test_ui(|ui| {
/// let selected = true;
/// ui.add(Button::new("toggle me").selected(selected).frame_when_inactive(!selected).frame(true));
/// # });
/// ```
///
/// See also:
/// - [`Ui::selectable_value`]
/// - [`Ui::selectable_label`]
pub fn selectable(selected: bool, atoms: impl IntoAtoms<'a>) -> Self {
Self::new(atoms)
.selected(selected)
.frame_when_inactive(selected)
.frame(true)
}
/// Creates a button with an image. The size of the image as displayed is defined by the provided size.
///
/// Note: In contrast to [`Button::new`], this limits the image size to the default font height
@@ -138,6 +161,18 @@ impl<'a> Button<'a> {
self
}
/// If `false`, the button will not have a frame when inactive.
///
/// Default: `true`.
///
/// Note: When [`Self::frame`] (or `ui.visuals().button_frame`) is `false`, this setting
/// has no effect.
#[inline]
pub fn frame_when_inactive(mut self, frame_when_inactive: bool) -> Self {
self.frame_when_inactive = frame_when_inactive;
self
}
/// By default, buttons senses clicks.
/// Change this to a drag-button with `Sense::drag()`.
#[inline]
@@ -220,6 +255,7 @@ impl<'a> Button<'a> {
stroke,
small,
frame,
frame_when_inactive,
mut min_size,
corner_radius,
selected,
@@ -243,9 +279,9 @@ impl<'a> Button<'a> {
let text = layout.text().map(String::from);
let has_frame = frame.unwrap_or_else(|| ui.visuals().button_frame);
let has_frame_margin = frame.unwrap_or_else(|| ui.visuals().button_frame);
let mut button_padding = if has_frame {
let mut button_padding = if has_frame_margin {
ui.spacing().button_padding
} else {
Vec2::ZERO
@@ -262,13 +298,22 @@ impl<'a> Button<'a> {
let response = if ui.is_rect_visible(prepared.response.rect) {
let visuals = ui.style().interact_selectable(&prepared.response, selected);
let visible_frame = if frame_when_inactive {
has_frame_margin
} else {
has_frame_margin
&& (prepared.response.hovered()
|| prepared.response.is_pointer_button_down_on()
|| prepared.response.has_focus())
};
if image_tint_follows_text_color {
prepared.map_images(|image| image.tint(visuals.text_color()));
}
prepared.fallback_text_color = visuals.text_color();
if has_frame {
if visible_frame {
let stroke = stroke.unwrap_or(visuals.bg_stroke);
let fill = fill.unwrap_or(visuals.weak_bg_fill);
prepared.frame = prepared

View File

@@ -1,6 +1,6 @@
use crate::{
epaint, pos2, Atom, AtomLayout, Atoms, Id, IntoAtoms, NumExt as _, Response, Sense, Shape, Ui,
Vec2, Widget, WidgetInfo, WidgetType,
Atom, AtomLayout, Atoms, Id, IntoAtoms, NumExt as _, Response, Sense, Shape, Ui, Vec2, Widget,
WidgetInfo, WidgetType, epaint, pos2,
};
// TODO(emilk): allow checkbox without a text label

View File

@@ -2,12 +2,13 @@
use crate::util::fixed_cache::FixedCache;
use crate::{
epaint, lerp, remap_clamp, Area, Context, DragValue, Frame, Id, Key, Order, Painter, Response,
Sense, Ui, UiKind, Widget as _, WidgetInfo, WidgetType,
Context, DragValue, Id, Painter, Popup, PopupCloseBehavior, Response, Sense, Ui, Widget as _,
WidgetInfo, WidgetType, epaint, lerp, remap_clamp,
};
use epaint::{
Mesh, Rect, Shape, Stroke, StrokeKind, Vec2,
ecolor::{Color32, Hsva, HsvaGamma, Rgba},
pos2, vec2, Mesh, Rect, Shape, Stroke, StrokeKind, Vec2,
pos2, vec2,
};
fn contrast_color(color: impl Into<Rgba>) -> Color32 {
@@ -492,41 +493,23 @@ pub fn color_picker_color32(ui: &mut Ui, srgba: &mut Color32, alpha: Alpha) -> b
pub fn color_edit_button_hsva(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) -> Response {
let popup_id = ui.auto_id_with("popup");
let open = ui.memory(|mem| mem.is_popup_open(popup_id));
let open = Popup::is_id_open(ui.ctx(), popup_id);
let mut button_response = color_button(ui, (*hsva).into(), open);
if ui.style().explanation_tooltips {
button_response = button_response.on_hover_text("Click to edit color");
}
if button_response.clicked() {
ui.memory_mut(|mem| mem.toggle_popup(popup_id));
}
const COLOR_SLIDER_WIDTH: f32 = 275.0;
// TODO(lucasmerlin): Update this to use new Popup struct
if ui.memory(|mem| mem.is_popup_open(popup_id)) {
ui.memory_mut(|mem| mem.keep_popup_open(popup_id));
let area_response = Area::new(popup_id)
.kind(UiKind::Picker)
.order(Order::Foreground)
.fixed_pos(button_response.rect.max)
.show(ui.ctx(), |ui| {
ui.spacing_mut().slider_width = COLOR_SLIDER_WIDTH;
Frame::popup(ui.style()).show(ui, |ui| {
if color_picker_hsva_2d(ui, hsva, alpha) {
button_response.mark_changed();
}
});
})
.response;
if !button_response.clicked()
&& (ui.input(|i| i.key_pressed(Key::Escape)) || area_response.clicked_elsewhere())
{
ui.memory_mut(|mem| mem.close_popup(popup_id));
}
}
Popup::menu(&button_response)
.id(popup_id)
.close_behavior(PopupCloseBehavior::CloseOnClickOutside)
.show(|ui| {
ui.spacing_mut().slider_width = COLOR_SLIDER_WIDTH;
if color_picker_hsva_2d(ui, hsva, alpha) {
button_response.mark_changed();
}
});
button_response
}

View File

@@ -3,8 +3,8 @@
use std::{cmp::Ordering, ops::RangeInclusive};
use crate::{
emath, text, Button, CursorIcon, Id, Key, Modifiers, NumExt as _, Response, RichText, Sense,
TextEdit, TextWrapMode, Ui, Widget, WidgetInfo, MINUS_CHAR_STR,
Button, CursorIcon, Id, Key, MINUS_CHAR_STR, Modifiers, NumExt as _, Response, RichText, Sense,
TextEdit, TextWrapMode, Ui, Widget, WidgetInfo, emath, text,
};
// ----------------------------------------------------------------------------

View File

@@ -1,6 +1,6 @@
use crate::{
epaint, text_selection, CursorIcon, Label, Response, Sense, Stroke, Ui, Widget, WidgetInfo,
WidgetText, WidgetType,
CursorIcon, Label, Response, Sense, Stroke, Ui, Widget, WidgetInfo, WidgetText, WidgetType,
epaint, text_selection,
};
use self::text_selection::LabelSelectionState;

View File

@@ -2,14 +2,15 @@ use std::{borrow::Cow, slice::Iter, sync::Arc, time::Duration};
use emath::{Align, Float as _, GuiRounding as _, NumExt as _, Rot2};
use epaint::{
text::{LayoutJob, TextFormat, TextWrapping},
RectShape,
text::{LayoutJob, TextFormat, TextWrapping},
};
use crate::{
load::{Bytes, SizeHint, SizedTexture, TextureLoadResult, TexturePoll},
pos2, Color32, Context, CornerRadius, Id, Mesh, Painter, Rect, Response, Sense, Shape, Spinner,
Color32, Context, CornerRadius, Id, Mesh, Painter, Rect, Response, Sense, Shape, Spinner,
TextStyle, TextureOptions, Ui, Vec2, Widget, WidgetInfo, WidgetType,
load::{Bytes, SizeHint, SizedTexture, TextureLoadResult, TexturePoll},
pos2,
};
/// A widget which displays an image.
@@ -277,7 +278,8 @@ impl<'a> Image<'a> {
}
/// Set alt text for the image. This will be shown when the image fails to load.
/// It will also be read to screen readers.
///
/// It will also be used for accessibility (e.g. read by screen readers).
#[inline]
pub fn alt_text(mut self, label: impl Into<String>) -> Self {
self.alt_text = Some(label.into());
@@ -499,7 +501,7 @@ impl ImageSize {
let point_size = match fit {
ImageFit::Original { scale } => {
return SizeHint::Scale((pixels_per_point * scale).ord())
return SizeHint::Scale((pixels_per_point * scale).ord());
}
ImageFit::Fraction(fract) => available_size * fract,
ImageFit::Exact(size) => size,
@@ -671,7 +673,7 @@ pub fn paint_texture_load_result(
rect: Rect,
show_loading_spinner: Option<bool>,
options: &ImageOptions,
alt: Option<&str>,
alt_text: Option<&str>,
) {
match tlr {
Ok(TexturePoll::Ready { texture }) => {
@@ -696,9 +698,9 @@ pub fn paint_texture_load_result(
0.0,
TextFormat::simple(font_id.clone(), ui.visuals().error_fg_color),
);
if let Some(alt) = alt {
if let Some(alt_text) = alt_text {
job.append(
alt,
alt_text,
ui.spacing().item_spacing.x,
TextFormat::simple(font_id, ui.visuals().text_color()),
);

View File

@@ -1,6 +1,6 @@
use crate::{
widgets, Color32, CornerRadius, Image, Rect, Response, Sense, Ui, Vec2, Widget, WidgetInfo,
WidgetType,
Color32, CornerRadius, Image, Rect, Response, Sense, Ui, Vec2, Widget, WidgetInfo, WidgetType,
widgets,
};
/// A clickable image within a frame.

View File

@@ -1,8 +1,8 @@
use std::sync::Arc;
use crate::{
epaint, pos2, text_selection::LabelSelectionState, Align, Direction, FontSelection, Galley,
Pos2, Response, Sense, Stroke, TextWrapMode, Ui, Widget, WidgetInfo, WidgetText, WidgetType,
Align, Direction, FontSelection, Galley, Pos2, Response, Sense, Stroke, TextWrapMode, Ui,
Widget, WidgetInfo, WidgetText, WidgetType, epaint, pos2, text_selection::LabelSelectionState,
};
/// Static text.
@@ -165,7 +165,7 @@ impl Label {
};
select_sense -= Sense::FOCUSABLE; // Don't move focus to labels with TAB key.
sense = sense.union(select_sense);
sense |= select_sense;
}
if let WidgetText::Galley(galley) = self.text {
@@ -219,11 +219,12 @@ impl Label {
let rect = galley.rows[0]
.rect_without_leading_space()
.translate(pos.to_vec2());
dbg!(galley.desired_size());
let mut response = ui.allocate_rect(rect, sense, galley.desired_size());
dbg!(galley.intrinsic_size());
let mut response = ui.allocate_rect(rect, sense, galley.intrinsic_size());
response.intrinsic_size = Some(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, galley.desired_size());
response |= ui.allocate_rect(rect, sense, galley.intrinsic_size());
}
(pos, galley, response)
} else {
@@ -253,8 +254,8 @@ impl Label {
};
let galley = ui.fonts(|fonts| fonts.layout_job(layout_job));
let (rect, response) =
ui.allocate_at_least(galley.size(), sense, galley.desired_size()); // TODO: Change back to allocate_exact_size
let (rect, mut response) = ui.allocate_at_least(galley.size(), sense, galley.intrinsic_size());
response.intrinsic_size = Some(galley.intrinsic_size());
let galley_pos = match galley.job.halign {
Align::LEFT => rect.left_top(),
Align::Center => rect.center_top(),

View File

@@ -4,7 +4,7 @@
//! * `ui.add(Label::new("Text").text_color(color::red));`
//! * `if ui.add(Button::new("Click me")).clicked() { … }`
use crate::{epaint, Response, Ui};
use crate::{Response, Ui, epaint};
mod button;
mod checkbox;
@@ -22,20 +22,21 @@ mod slider;
mod spinner;
pub mod text_edit;
#[expect(deprecated)]
pub use self::selected_label::SelectableLabel;
pub use self::{
button::Button,
checkbox::Checkbox,
drag_value::DragValue,
hyperlink::{Hyperlink, Link},
image::{
decode_animated_image_uri, has_gif_magic_header, has_webp_header, paint_texture_at,
FrameDurations, Image, ImageFit, ImageOptions, ImageSize, ImageSource,
decode_animated_image_uri, has_gif_magic_header, has_webp_header, paint_texture_at,
},
image_button::ImageButton,
label::Label,
progress_bar::ProgressBar,
radio_button::RadioButton,
selected_label::SelectableLabel,
separator::Separator,
slider::{Slider, SliderClamping, SliderOrientation},
spinner::Spinner,

View File

@@ -1,6 +1,6 @@
use crate::{
lerp, vec2, Color32, CornerRadius, NumExt as _, Pos2, Rect, Response, Rgba, Sense, Shape,
Stroke, TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetType,
Color32, CornerRadius, NumExt as _, Pos2, Rect, Response, Rgba, Sense, Shape, Stroke,
TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetType, lerp, vec2,
};
enum ProgressBarText {

View File

@@ -1,6 +1,6 @@
use crate::{
epaint, Atom, AtomLayout, Atoms, Id, IntoAtoms, NumExt as _, Response, Sense, Ui, Vec2, Widget,
WidgetInfo, WidgetType,
Atom, AtomLayout, Atoms, Id, IntoAtoms, NumExt as _, Response, Sense, Ui, Vec2, Widget,
WidgetInfo, WidgetType, epaint,
};
/// One out of several alternatives, either selected or not.

View File

@@ -1,89 +1,13 @@
use crate::{
NumExt as _, Response, Sense, TextStyle, Ui, Widget, WidgetInfo, WidgetText, WidgetType,
};
#![expect(deprecated, clippy::new_ret_no_self)]
/// One out of several alternatives, either selected or not.
/// Will mark selected items with a different background color.
/// An alternative to [`crate::RadioButton`] and [`crate::Checkbox`].
///
/// Usually you'd use [`Ui::selectable_value`] or [`Ui::selectable_label`] instead.
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// #[derive(PartialEq)]
/// enum Enum { First, Second, Third }
/// let mut my_enum = Enum::First;
///
/// ui.selectable_value(&mut my_enum, Enum::First, "First");
///
/// // is equivalent to:
///
/// if ui.add(egui::SelectableLabel::new(my_enum == Enum::First, "First")).clicked() {
/// my_enum = Enum::First
/// }
/// # });
/// ```
#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
pub struct SelectableLabel {
selected: bool,
text: WidgetText,
}
use crate::WidgetText;
#[deprecated = "Use `Button::selectable()` instead"]
pub struct SelectableLabel {}
impl SelectableLabel {
pub fn new(selected: bool, text: impl Into<WidgetText>) -> Self {
Self {
selected,
text: text.into(),
}
}
}
impl Widget for SelectableLabel {
fn ui(self, ui: &mut Ui) -> Response {
let Self { selected, text } = self;
let button_padding = ui.spacing().button_padding;
let total_extra = button_padding + button_padding;
let wrap_width = ui.available_width() - total_extra.x;
let galley = text.into_galley(ui, None, wrap_width, TextStyle::Button);
let mut desired_size = total_extra + galley.size();
desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y);
let (rect, response) =
ui.allocate_at_least(desired_size, Sense::click(), galley.desired_size());
response.widget_info(|| {
WidgetInfo::selected(
WidgetType::SelectableLabel,
ui.is_enabled(),
selected,
galley.text(),
)
});
if ui.is_rect_visible(response.rect) {
let text_pos = ui
.layout()
.align_size_within_rect(galley.size(), rect.shrink2(button_padding))
.min;
let visuals = ui.style().interact_selectable(&response, selected);
if selected || response.hovered() || response.highlighted() || response.has_focus() {
let rect = rect.expand(visuals.expansion);
ui.painter().rect(
rect,
visuals.corner_radius,
visuals.weak_bg_fill,
visuals.bg_stroke,
epaint::StrokeKind::Inside,
);
}
ui.painter().galley(text_pos, galley, visuals.text_color());
}
response
#[deprecated = "Use `Button::selectable()` instead"]
pub fn new(selected: bool, text: impl Into<WidgetText>) -> super::Button<'static> {
crate::Button::selectable(selected, text)
}
}

View File

@@ -1,4 +1,4 @@
use crate::{vec2, Response, Sense, Ui, Vec2, Widget};
use crate::{Response, Sense, Ui, Vec2, Widget, vec2};
/// A visual separator. A horizontal or vertical line (depending on [`crate::Layout`]).
///

View File

@@ -3,9 +3,9 @@
use std::ops::RangeInclusive;
use crate::{
emath, epaint, lerp, pos2, remap, remap_clamp, style, style::HandleShape, vec2, Color32,
DragValue, EventFilter, Key, Label, NumExt as _, Pos2, Rangef, Rect, Response, Sense,
TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, MINUS_CHAR_STR,
Color32, DragValue, EventFilter, Key, Label, MINUS_CHAR_STR, NumExt as _, Pos2, Rangef, Rect,
Response, Sense, TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, emath,
epaint, lerp, pos2, remap, remap_clamp, style, style::HandleShape, vec2,
};
use super::drag_value::clamp_value_to_range;
@@ -134,11 +134,7 @@ impl<'a> Slider<'a> {
value.to_f64()
});
if Num::INTEGRAL {
slf.integer()
} else {
slf
}
if Num::INTEGRAL { slf.integer() } else { slf }
}
pub fn from_get_set(
@@ -736,7 +732,7 @@ impl Slider<'_> {
let prev_value = self.get_value();
let prev_position = self.position_from_value(prev_value, position_range);
let new_position = prev_position + ui_point_per_step * kb_step;
let new_value = match self.step {
let mut new_value = match self.step {
Some(step) => prev_value + (kb_step as f64 * step),
None if self.smart_aim => {
let aim_radius = 0.49 * ui_point_per_step; // Chosen so we don't include `prev_value` in the search.
@@ -747,6 +743,19 @@ impl Slider<'_> {
}
_ => self.value_from_position(new_position, position_range),
};
if let Some(max_decimals) = self.max_decimals {
// self.set_value rounds, so ensure we reach at the least the next breakpoint
// note: we give it a little bit of leeway due to floating point errors. (0.1 isn't representable in binary)
// 'set_value' will round it to the nearest value.
let min_increment = 1.0 / (10.0_f64.powi(max_decimals as i32));
new_value = if new_value > prev_value {
f64::max(new_value, prev_value + min_increment * 1.001)
} else if new_value < prev_value {
f64::min(new_value, prev_value - min_increment * 1.001)
} else {
new_value
};
}
self.set_value(new_value);
}

View File

@@ -1,4 +1,4 @@
use epaint::{emath::lerp, vec2, Color32, Pos2, Rect, Shape, Stroke};
use epaint::{Color32, Pos2, Rect, Shape, Stroke, emath::lerp, vec2};
use crate::{Response, Sense, Ui, Widget, WidgetInfo, WidgetType};

View File

@@ -2,19 +2,19 @@ use std::sync::Arc;
use emath::{Rect, TSTransform};
use epaint::{
text::{cursor::CCursor, Galley, LayoutJob},
StrokeKind,
text::{Galley, LayoutJob, cursor::CCursor},
};
use crate::{
epaint,
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,
os::OperatingSystem,
output::OutputEvent,
response, text_selection,
text_selection::{text_cursor_state::cursor_rect, visuals::paint_text_selection, CCursorRange},
vec2, 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,
text_selection::{CCursorRange, text_cursor_state::cursor_rect, visuals::paint_text_selection},
vec2,
};
use super::{TextEditOutput, TextEditState};
@@ -63,7 +63,7 @@ type LayouterFn<'t> = &'t mut dyn FnMut(&Ui, &dyn TextBuffer, f32) -> Arc<Galley
/// See [`TextEdit::show`].
///
/// ## Other
/// The background color of a [`crate::TextEdit`] is [`crate::Visuals::extreme_bg_color`] or can be set with [`crate::TextEdit::background_color`].
/// The background color of a [`crate::TextEdit`] is [`crate::Visuals::text_edit_bg_color`] or can be set with [`crate::TextEdit::background_color`].
#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
pub struct TextEdit<'t> {
text: &'t mut dyn TextBuffer,
@@ -207,7 +207,7 @@ impl<'t> TextEdit<'t> {
self
}
/// Set the background color of the [`TextEdit`]. The default is [`crate::Visuals::extreme_bg_color`].
/// Set the background color of the [`TextEdit`]. The default is [`crate::Visuals::text_edit_bg_color`].
// TODO(bircni): remove this once #3284 is implemented
#[inline]
pub fn background_color(mut self, color: Color32) -> Self {
@@ -428,7 +428,7 @@ impl TextEdit<'_> {
let where_to_put_background = ui.painter().add(Shape::Noop);
let background_color = self
.background_color
.unwrap_or(ui.visuals().extreme_bg_color);
.unwrap_or_else(|| ui.visuals().text_edit_bg_color());
let output = self.show_content(ui);
if frame {

View File

@@ -3,8 +3,8 @@ use std::sync::Arc;
use crate::mutex::Mutex;
use crate::{
text_selection::{CCursorRange, TextCursorState},
Context, Id,
text_selection::{CCursorRange, TextCursorState},
};
pub type TextEditUndoer = crate::util::undoer::Undoer<(CCursorRange, String)>;

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