1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-30 13:20:05 -04:00

Merge remote-tracking branch 'origin/master' into default_affects_opacity

This commit is contained in:
tye-exe
2025-06-17 13:24:41 +01:00
285 changed files with 5692 additions and 2237 deletions

View File

@@ -102,9 +102,6 @@ impl Color32 {
/// i.e. often taken to mean "no color".
pub const PLACEHOLDER: Self = Self::from_rgba_premultiplied(64, 254, 0, 128);
#[deprecated = "Renamed to PLACEHOLDER"]
pub const TEMPORARY_COLOR: Self = Self::PLACEHOLDER;
/// From RGB with alpha of 255 (opaque).
#[inline]
pub const fn from_rgb(r: u8, g: u8, b: u8) -> Self {

View File

@@ -4,6 +4,7 @@ use crate::{
/// Hue, saturation, value, alpha. All in the range [0, 1].
/// No premultiplied alpha.
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Hsva {
/// hue 0-1

View File

@@ -33,12 +33,11 @@ pub(crate) fn f32_hash<H: std::hash::Hasher>(state: &mut H, f: f32) {
} else if f.is_nan() {
state.write_u8(1);
} else {
use std::hash::Hash;
use std::hash::Hash as _;
f.to_bits().hash(state);
}
}
#[allow(clippy::derived_hash_with_manual_eq)]
impl std::hash::Hash for Rgba {
#[inline]
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {

View File

@@ -5,10 +5,10 @@ authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "egui framework - write GUI apps that compiles to web and/or natively"
edition.workspace = true
rust-version.workspace = true
homepage = "https://github.com/emilk/egui/tree/master/crates/eframe"
homepage = "https://github.com/emilk/egui/tree/main/crates/eframe"
license.workspace = true
readme = "README.md"
repository = "https://github.com/emilk/egui/tree/master/crates/eframe"
repository = "https://github.com/emilk/egui/tree/main/crates/eframe"
categories = ["gui", "game-development"]
keywords = ["egui", "gui", "gamedev"]
include = [
@@ -59,7 +59,7 @@ android-native-activity = ["egui-winit/android-native-activity"]
## If you plan on specifying your own fonts you may disable this feature.
default_fonts = ["egui/default_fonts"]
## Use [`glow`](https://github.com/grovesNL/glow) for painting, via [`egui_glow`](https://github.com/emilk/egui/tree/master/crates/egui_glow).
## Use [`glow`](https://github.com/grovesNL/glow) for painting, via [`egui_glow`](https://github.com/emilk/egui/tree/main/crates/egui_glow).
glow = ["dep:egui_glow", "dep:glow", "dep:glutin-winit", "dep:glutin"]
## Enable saving app state to disk.
@@ -90,7 +90,7 @@ web_screen_reader = [
"web-sys/SpeechSynthesisUtterance",
]
## Use [`wgpu`](https://docs.rs/wgpu) for painting (via [`egui-wgpu`](https://github.com/emilk/egui/tree/master/crates/egui-wgpu)).
## Use [`wgpu`](https://docs.rs/wgpu) for painting (via [`egui-wgpu`](https://github.com/emilk/egui/tree/main/crates/egui-wgpu)).
##
## This overrides the `glow` feature.
##

View File

@@ -7,7 +7,7 @@
`eframe` is the official framework library for writing apps using [`egui`](https://github.com/emilk/egui). The app can be compiled both to run natively (for Linux, Mac, Windows, and Android) or as a web app (using [Wasm](https://en.wikipedia.org/wiki/WebAssembly)).
To get started, see the [examples](https://github.com/emilk/egui/tree/master/examples).
To get started, see the [examples](https://github.com/emilk/egui/tree/main/examples).
To learn how to set up `eframe` for web and native, go to <https://github.com/emilk/eframe_template/> and follow the instructions there!
There is also a tutorial video at <https://www.youtube.com/watch?v=NtUkr_z7l84>.
@@ -16,7 +16,7 @@ For how to use `egui`, see [the egui docs](https://docs.rs/egui).
---
`eframe` uses [`egui_glow`](https://github.com/emilk/egui/tree/master/crates/egui_glow) for rendering, and on native it uses [`egui-winit`](https://github.com/emilk/egui/tree/master/crates/egui-winit).
`eframe` uses [`egui_glow`](https://github.com/emilk/egui/tree/main/crates/egui_glow) for rendering, and on native it uses [`egui-winit`](https://github.com/emilk/egui/tree/main/crates/egui-winit).
To use on Linux, first run:
@@ -26,12 +26,12 @@ sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev lib
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 can opt-in to the using [`egui-wgpu`](https://github.com/emilk/egui/tree/master/crates/egui-wgpu) for rendering by enabling the `wgpu` feature and setting `NativeOptions::renderer` to `Renderer::Wgpu`.
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`.
## Alternatives
`eframe` is not the only way to write an app using `egui`! You can also try [`egui-miniquad`](https://github.com/not-fl3/egui-miniquad), [`bevy_egui`](https://github.com/mvlabat/bevy_egui), [`egui_sdl2_gl`](https://github.com/ArjunNair/egui_sdl2_gl), and others.
You can also use `egui_glow` and [`winit`](https://github.com/rust-windowing/winit) to build your own app as demonstrated in <https://github.com/emilk/egui/blob/master/crates/egui_glow/examples/pure_glow.rs>.
You can also use `egui_glow` and [`winit`](https://github.com/rust-windowing/winit) to build your own app as demonstrated in <https://github.com/emilk/egui/blob/main/crates/egui_glow/examples/pure_glow.rs>.
## Limitations when running egui on the web

View File

@@ -91,7 +91,7 @@ pub struct CreationContext<'s> {
pub(crate) raw_display_handle: Result<RawDisplayHandle, HandleError>,
}
#[allow(unsafe_code)]
#[expect(unsafe_code)]
#[cfg(not(target_arch = "wasm32"))]
impl HasWindowHandle for CreationContext<'_> {
fn window_handle(&self) -> Result<WindowHandle<'_>, HandleError> {
@@ -100,7 +100,7 @@ impl HasWindowHandle for CreationContext<'_> {
}
}
#[allow(unsafe_code)]
#[expect(unsafe_code)]
#[cfg(not(target_arch = "wasm32"))]
impl HasDisplayHandle for CreationContext<'_> {
fn display_handle(&self) -> Result<DisplayHandle<'_>, HandleError> {
@@ -133,7 +133,7 @@ impl CreationContext<'_> {
// ----------------------------------------------------------------------------
/// Implement this trait to write apps that can be compiled for both web/wasm and desktop/native using [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe).
/// Implement this trait to write apps that can be compiled for both web/wasm and desktop/native using [`eframe`](https://github.com/emilk/egui/tree/main/crates/eframe).
pub trait App {
/// Called each time the UI needs repainting, which may be many times per second.
///
@@ -662,7 +662,7 @@ pub struct Frame {
#[cfg(not(target_arch = "wasm32"))]
assert_not_impl_any!(Frame: Clone);
#[allow(unsafe_code)]
#[expect(unsafe_code)]
#[cfg(not(target_arch = "wasm32"))]
impl HasWindowHandle for Frame {
fn window_handle(&self) -> Result<WindowHandle<'_>, HandleError> {
@@ -671,7 +671,7 @@ impl HasWindowHandle for Frame {
}
}
#[allow(unsafe_code)]
#[expect(unsafe_code)]
#[cfg(not(target_arch = "wasm32"))]
impl HasDisplayHandle for Frame {
fn display_handle(&self) -> Result<DisplayHandle<'_>, HandleError> {
@@ -703,7 +703,7 @@ impl Frame {
/// True if you are in a web environment.
///
/// Equivalent to `cfg!(target_arch = "wasm32")`
#[allow(clippy::unused_self)]
#[expect(clippy::unused_self)]
pub fn is_web(&self) -> bool {
cfg!(target_arch = "wasm32")
}

View File

@@ -3,7 +3,7 @@
//! If you are planning to write an app for web or native,
//! and want to use [`egui`] for everything, then `eframe` is for you!
//!
//! To get started, see the [examples](https://github.com/emilk/egui/tree/master/examples).
//! To get started, see the [examples](https://github.com/emilk/egui/tree/main/examples).
//! To learn how to set up `eframe` for web and native, go to <https://github.com/emilk/eframe_template/> and follow the instructions there!
//!
//! In short, you implement [`App`] (especially [`App::update`]) and then
@@ -69,7 +69,7 @@
//! #[wasm_bindgen]
//! impl WebHandle {
//! /// Installs a panic hook, then returns.
//! #[allow(clippy::new_without_default)]
//! #[expect(clippy::new_without_default)]
//! #[wasm_bindgen(constructor)]
//! pub fn new() -> Self {
//! // Redirect [`log`] message to `console.log` and friends:
@@ -144,6 +144,15 @@
#![warn(missing_docs)] // let's keep eframe well-documented
#![allow(clippy::needless_doctest_main)]
// Limitation imposed by `accesskit_winit`:
// https://github.com/AccessKit/accesskit/tree/accesskit-v0.18.0/platforms/winit#android-activity-compatibility`
#[cfg(all(
target_os = "android",
feature = "accesskit",
feature = "android-native-activity"
))]
compile_error!("`accesskit` feature is only available with `android-game-activity`");
// Re-export all useful libraries:
pub use {egui, egui::emath, egui::epaint};
@@ -182,6 +191,14 @@ pub use web::{WebLogger, WebRunner};
#[cfg(any(feature = "glow", feature = "wgpu"))]
mod native;
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu"))]
pub use native::run::EframeWinitApplication;
#[cfg(not(any(target_arch = "wasm32", target_os = "ios")))]
#[cfg(any(feature = "glow", feature = "wgpu"))]
pub use native::run::EframePumpStatus;
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[cfg(feature = "persistence")]
@@ -192,7 +209,7 @@ pub mod icon_data;
/// This is how you start a native (desktop) app.
///
/// The first argument is name of your app, which is a an identifier
/// The first argument is name of your app, which is an identifier
/// used for the save location of persistence (see [`App::save`]).
/// It is also used as the application id on wayland.
/// If you set no title on the viewport, the app id will be used
@@ -236,32 +253,13 @@ pub mod icon_data;
/// This function can fail if we fail to set up a graphics context.
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[allow(clippy::needless_pass_by_value)]
#[allow(clippy::needless_pass_by_value, clippy::allow_attributes)]
pub fn run_native(
app_name: &str,
mut native_options: NativeOptions,
app_creator: AppCreator<'_>,
) -> Result {
#[cfg(not(feature = "__screenshot"))]
assert!(
std::env::var("EFRAME_SCREENSHOT_TO").is_err(),
"EFRAME_SCREENSHOT_TO found without compiling with the '__screenshot' feature"
);
if native_options.viewport.title.is_none() {
native_options.viewport.title = Some(app_name.to_owned());
}
let renderer = native_options.renderer;
#[cfg(all(feature = "glow", feature = "wgpu"))]
{
match renderer {
Renderer::Glow => "glow",
Renderer::Wgpu => "wgpu",
};
log::info!("Both the glow and wgpu renderers are available. Using {renderer}.");
}
let renderer = init_native(app_name, &mut native_options);
match renderer {
#[cfg(feature = "glow")]
@@ -278,6 +276,113 @@ pub fn run_native(
}
}
/// Provides a proxy for your native eframe application to run on your own event loop.
///
/// See `run_native` for details about `app_name`.
///
/// Call from `fn main` like this:
/// ``` no_run
/// use eframe::{egui, UserEvent};
/// use winit::event_loop::{ControlFlow, EventLoop};
///
/// fn main() -> eframe::Result {
/// let native_options = eframe::NativeOptions::default();
/// let eventloop = EventLoop::<UserEvent>::with_user_event().build()?;
/// eventloop.set_control_flow(ControlFlow::Poll);
///
/// let mut winit_app = eframe::create_native(
/// "MyExtApp",
/// native_options,
/// Box::new(|cc| Ok(Box::new(MyEguiApp::new(cc)))),
/// &eventloop,
/// );
///
/// eventloop.run_app(&mut winit_app)?;
///
/// Ok(())
/// }
///
/// #[derive(Default)]
/// struct MyEguiApp {}
///
/// impl MyEguiApp {
/// fn new(cc: &eframe::CreationContext<'_>) -> Self {
/// Self::default()
/// }
/// }
///
/// impl eframe::App for MyEguiApp {
/// fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
/// egui::CentralPanel::default().show(ctx, |ui| {
/// ui.heading("Hello World!");
/// });
/// }
/// }
/// ```
///
/// See the `external_eventloop` example for a more complete example.
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu"))]
pub fn create_native<'a>(
app_name: &str,
mut native_options: NativeOptions,
app_creator: AppCreator<'a>,
event_loop: &winit::event_loop::EventLoop<UserEvent>,
) -> EframeWinitApplication<'a> {
let renderer = init_native(app_name, &mut native_options);
match renderer {
#[cfg(feature = "glow")]
Renderer::Glow => {
log::debug!("Using the glow renderer");
EframeWinitApplication::new(native::run::create_glow(
app_name,
native_options,
app_creator,
event_loop,
))
}
#[cfg(feature = "wgpu")]
Renderer::Wgpu => {
log::debug!("Using the wgpu renderer");
EframeWinitApplication::new(native::run::create_wgpu(
app_name,
native_options,
app_creator,
event_loop,
))
}
}
}
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu"))]
fn init_native(app_name: &str, native_options: &mut NativeOptions) -> Renderer {
#[cfg(not(feature = "__screenshot"))]
assert!(
std::env::var("EFRAME_SCREENSHOT_TO").is_err(),
"EFRAME_SCREENSHOT_TO found without compiling with the '__screenshot' feature"
);
if native_options.viewport.title.is_none() {
native_options.viewport.title = Some(app_name.to_owned());
}
let renderer = native_options.renderer;
#[cfg(all(feature = "glow", feature = "wgpu"))]
{
match native_options.renderer {
Renderer::Glow => "glow",
Renderer::Wgpu => "wgpu",
};
log::info!("Both the glow and wgpu renderers are available. Using {renderer}.");
}
renderer
}
// ----------------------------------------------------------------------------
/// The simplest way to get started when writing a native app.

View File

@@ -47,7 +47,7 @@ enum AppIconStatus {
NotSetTryAgain,
/// We successfully set the icon and it should be visible now.
#[allow(dead_code)] // Not used on Linux
#[allow(dead_code, clippy::allow_attributes)] // Not used on Linux
Set,
}
@@ -71,13 +71,13 @@ fn set_title_and_icon(_title: &str, _icon_data: Option<&IconData>) -> AppIconSta
#[cfg(target_os = "macos")]
return set_title_and_icon_mac(_title, _icon_data);
#[allow(unreachable_code)]
#[allow(unreachable_code, clippy::allow_attributes)]
AppIconStatus::NotSetIgnored
}
/// Set icon for Windows applications.
#[cfg(target_os = "windows")]
#[allow(unsafe_code)]
#[expect(unsafe_code)]
fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
use crate::icon_data::IconDataExt as _;
use winapi::um::winuser;
@@ -198,12 +198,12 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
/// Set icon & app title for `MacOS` applications.
#[cfg(target_os = "macos")]
#[allow(unsafe_code)]
#[expect(unsafe_code)]
fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconStatus {
use crate::icon_data::IconDataExt as _;
profiling::function_scope!();
use objc2::ClassType;
use objc2::ClassType as _;
use objc2_app_kit::{NSApplication, NSImage};
use objc2_foundation::{NSData, NSString};

View File

@@ -136,7 +136,7 @@ pub fn create_storage(_app_name: &str) -> Option<Box<dyn epi::Storage>> {
None
}
#[allow(clippy::unnecessary_wraps)]
#[expect(clippy::unnecessary_wraps)]
pub fn create_storage_with_file(_file: impl Into<PathBuf>) -> Option<Box<dyn epi::Storage>> {
#[cfg(feature = "persistence")]
return Some(Box::new(
@@ -169,7 +169,7 @@ pub struct EpiIntegration {
}
impl EpiIntegration {
#[allow(clippy::too_many_arguments)]
#[expect(clippy::too_many_arguments)]
pub fn new(
egui_ctx: egui::Context,
window: &winit::window::Window,
@@ -326,7 +326,7 @@ impl EpiIntegration {
}
}
#[allow(clippy::unused_self)]
#[allow(clippy::unused_self, clippy::allow_attributes)]
pub fn save(&mut self, _app: &mut dyn epi::App, _window: Option<&winit::window::Window>) {
#[cfg(feature = "persistence")]
if let Some(storage) = self.frame.storage_mut() {

View File

@@ -27,7 +27,7 @@ impl Drop for EventLoopGuard {
}
// Helper function to safely use the current event loop
#[allow(unsafe_code)]
#[expect(unsafe_code)]
pub fn with_current_event_loop<F, R>(f: F) -> Option<R>
where
F: FnOnce(&ActiveEventLoop) -> R,

View File

@@ -1,6 +1,6 @@
use std::{
collections::HashMap,
io::Write,
io::Write as _,
path::{Path, PathBuf},
};
@@ -42,10 +42,10 @@ pub fn storage_dir(app_id: &str) -> Option<PathBuf> {
// Adapted from
// https://github.com/rust-lang/cargo/blob/6e11c77384989726bb4f412a0e23b59c27222c34/crates/home/src/windows.rs#L19-L37
#[cfg(all(windows, not(target_vendor = "uwp")))]
#[allow(unsafe_code)]
#[expect(unsafe_code)]
fn roaming_appdata() -> Option<PathBuf> {
use std::ffi::OsString;
use std::os::windows::ffi::OsStringExt;
use std::os::windows::ffi::OsStringExt as _;
use std::ptr;
use std::slice;
@@ -207,7 +207,8 @@ fn save_to_disk(file_path: &PathBuf, kv: &HashMap<String, String>) {
let config = Default::default();
profiling::scope!("ron::serialize");
if let Err(err) = ron::ser::to_writer_pretty(&mut writer, &kv, config)
if let Err(err) = ron::Options::default()
.to_io_writer_pretty(&mut writer, &kv, config)
.and_then(|_| writer.flush().map_err(|err| err.into()))
{
log::warn!("Failed to serialize app state: {}", err);

View File

@@ -11,13 +11,13 @@ use std::{cell::RefCell, num::NonZeroU32, rc::Rc, sync::Arc, time::Instant};
use egui_winit::ActionRequested;
use glutin::{
config::GlConfig,
context::NotCurrentGlContext,
display::GetGlDisplay,
prelude::{GlDisplay, PossiblyCurrentGlContext},
surface::GlSurface,
config::GlConfig as _,
context::NotCurrentGlContext as _,
display::GetGlDisplay as _,
prelude::{GlDisplay as _, PossiblyCurrentGlContext as _},
surface::GlSurface as _,
};
use raw_window_handle::HasWindowHandle;
use raw_window_handle::HasWindowHandle as _;
use winit::{
event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy},
window::{Window, WindowId},
@@ -139,7 +139,7 @@ impl<'app> GlowWinitApp<'app> {
}
}
#[allow(unsafe_code)]
#[expect(unsafe_code)]
fn create_glutin_windowed_context(
egui_ctx: &egui::Context,
event_loop: &ActiveEventLoop,
@@ -901,7 +901,7 @@ fn change_gl_context(
}
impl GlutinWindowContext {
#[allow(unsafe_code)]
#[expect(unsafe_code)]
unsafe fn new(
egui_ctx: &egui::Context,
viewport_builder: ViewportBuilder,
@@ -1094,7 +1094,7 @@ impl GlutinWindowContext {
}
/// Create a surface, window, and winit integration for the viewport, if missing.
#[allow(unsafe_code)]
#[expect(unsafe_code)]
pub(crate) fn initialize_window(
&mut self,
viewport_id: ViewportId,
@@ -1566,6 +1566,6 @@ fn save_screenshot_and_exit(
});
log::info!("Screenshot saved to {path:?}.");
#[allow(clippy::exit)]
#[expect(clippy::exit)]
std::process::exit(0);
}

View File

@@ -163,7 +163,6 @@ impl<T: WinitApp> WinitAppWrapper<T> {
log::debug!("Exiting with return code 0");
#[allow(clippy::exit)]
std::process::exit(0);
}
}
@@ -317,7 +316,7 @@ impl<T: WinitApp> ApplicationHandler<UserEvent> for WinitAppWrapper<T> {
#[cfg(not(target_os = "ios"))]
fn run_and_return(event_loop: &mut EventLoop<UserEvent>, winit_app: impl WinitApp) -> Result {
use winit::platform::run_on_demand::EventLoopExtRunOnDemand;
use winit::platform::run_on_demand::EventLoopExtRunOnDemand as _;
log::trace!("Entering the winit event loop (run_app_on_demand)…");
@@ -363,6 +362,19 @@ pub fn run_glow(
run_and_exit(event_loop, glow_eframe)
}
#[cfg(feature = "glow")]
pub fn create_glow<'a>(
app_name: &str,
native_options: epi::NativeOptions,
app_creator: epi::AppCreator<'a>,
event_loop: &EventLoop<UserEvent>,
) -> impl ApplicationHandler<UserEvent> + 'a {
use super::glow_integration::GlowWinitApp;
let glow_eframe = GlowWinitApp::new(event_loop, app_name, native_options, app_creator);
WinitAppWrapper::new(glow_eframe, true)
}
// ----------------------------------------------------------------------------
#[cfg(feature = "wgpu")]
@@ -387,3 +399,120 @@ pub fn run_wgpu(
let wgpu_eframe = WgpuWinitApp::new(&event_loop, app_name, native_options, app_creator);
run_and_exit(event_loop, wgpu_eframe)
}
#[cfg(feature = "wgpu")]
pub fn create_wgpu<'a>(
app_name: &str,
native_options: epi::NativeOptions,
app_creator: epi::AppCreator<'a>,
event_loop: &EventLoop<UserEvent>,
) -> impl ApplicationHandler<UserEvent> + 'a {
use super::wgpu_integration::WgpuWinitApp;
let wgpu_eframe = WgpuWinitApp::new(event_loop, app_name, native_options, app_creator);
WinitAppWrapper::new(wgpu_eframe, true)
}
// ----------------------------------------------------------------------------
/// A proxy to the eframe application that implements [`ApplicationHandler`].
///
/// This can be run directly on your own [`EventLoop`] by itself or with other
/// windows you manage outside of eframe.
pub struct EframeWinitApplication<'a> {
wrapper: Box<dyn ApplicationHandler<UserEvent> + 'a>,
control_flow: ControlFlow,
}
impl ApplicationHandler<UserEvent> for EframeWinitApplication<'_> {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
self.wrapper.resumed(event_loop);
}
fn window_event(
&mut self,
event_loop: &ActiveEventLoop,
window_id: winit::window::WindowId,
event: winit::event::WindowEvent,
) {
self.wrapper.window_event(event_loop, window_id, event);
}
fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: winit::event::StartCause) {
self.wrapper.new_events(event_loop, cause);
}
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) {
self.wrapper.user_event(event_loop, event);
}
fn device_event(
&mut self,
event_loop: &ActiveEventLoop,
device_id: winit::event::DeviceId,
event: winit::event::DeviceEvent,
) {
self.wrapper.device_event(event_loop, device_id, event);
}
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
self.wrapper.about_to_wait(event_loop);
self.control_flow = event_loop.control_flow();
}
fn suspended(&mut self, event_loop: &ActiveEventLoop) {
self.wrapper.suspended(event_loop);
}
fn exiting(&mut self, event_loop: &ActiveEventLoop) {
self.wrapper.exiting(event_loop);
}
fn memory_warning(&mut self, event_loop: &ActiveEventLoop) {
self.wrapper.memory_warning(event_loop);
}
}
impl<'a> EframeWinitApplication<'a> {
pub(crate) fn new<T: ApplicationHandler<UserEvent> + 'a>(app: T) -> Self {
Self {
wrapper: Box::new(app),
control_flow: ControlFlow::default(),
}
}
/// Pump the `EventLoop` to check for and dispatch pending events to this application.
///
/// Returns either the exit code for the application or the final state of the [`ControlFlow`]
/// after all events have been dispatched in this iteration.
///
/// This is useful when your [`EventLoop`] is not the main event loop for your application.
/// See the `external_eventloop_async` example.
#[cfg(not(target_os = "ios"))]
pub fn pump_eframe_app(
&mut self,
event_loop: &mut EventLoop<UserEvent>,
timeout: Option<std::time::Duration>,
) -> EframePumpStatus {
use winit::platform::pump_events::{EventLoopExtPumpEvents as _, PumpStatus};
match event_loop.pump_app_events(timeout, self) {
PumpStatus::Continue => EframePumpStatus::Continue(self.control_flow),
PumpStatus::Exit(code) => EframePumpStatus::Exit(code),
}
}
}
/// Either an exit code or a [`ControlFlow`] from the [`ActiveEventLoop`].
///
/// The result of [`EframeWinitApplication::pump_eframe_app`].
#[cfg(not(target_os = "ios"))]
pub enum EframePumpStatus {
/// The final state of the [`ControlFlow`] after all events have been dispatched
///
/// Callers should perform the action that is appropriate for the [`ControlFlow`] value.
Continue(ControlFlow),
/// The exit code for the application
Exit(i32),
}

View File

@@ -15,7 +15,7 @@ use winit::{
window::{Window, WindowId},
};
use ahash::{HashMap, HashSet, HashSetExt};
use ahash::{HashMap, HashSet, HashSetExt as _};
use egui::{
DeferredViewportUiCallback, FullOutput, ImmediateViewport, ViewportBuilder, ViewportClass,
ViewportId, ViewportIdMap, ViewportIdPair, ViewportIdSet, ViewportInfo, ViewportOutput,
@@ -182,7 +182,6 @@ impl<'app> WgpuWinitApp<'app> {
builder: ViewportBuilder,
) -> crate::Result<&mut WgpuWinitRunning<'app>> {
profiling::function_scope!();
#[allow(unsafe_code, unused_mut, unused_unsafe)]
let mut painter = pollster::block_on(egui_wgpu::winit::Painter::new(
egui_ctx.clone(),
self.native_options.wgpu_options.clone(),
@@ -236,7 +235,7 @@ impl<'app> WgpuWinitApp<'app> {
});
}
#[allow(unused_mut)] // used for accesskit
#[allow(unused_mut, clippy::allow_attributes)] // used for accesskit
let mut egui_winit = egui_winit::State::new(
egui_ctx.clone(),
ViewportId::ROOT,

View File

@@ -2,10 +2,10 @@ use egui::{TexturesDelta, UserData, ViewportCommand};
use crate::{epi, App};
use super::{now_sec, text_agent::TextAgent, web_painter::WebPainter, NeedRepaint};
use super::{now_sec, text_agent::TextAgent, web_painter::WebPainter as _, NeedRepaint};
pub struct AppRunner {
#[allow(dead_code)]
#[allow(dead_code, clippy::allow_attributes)]
pub(crate) web_options: crate::WebOptions,
pub(crate) frame: epi::Frame,
egui_ctx: egui::Context,
@@ -336,9 +336,6 @@ impl AppRunner {
egui::OutputCommand::OpenUrl(open_url) => {
super::open_url(&open_url.url, open_url.new_tab);
}
egui::OutputCommand::SetPointerPosition(_) => {
// Not supported on the web.
}
}
}

View File

@@ -4,7 +4,7 @@ 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, JsValue, WebRunner,
theme_from_dark_mode, translate_key, AppRunner, Closure, JsCast as _, JsValue, WebRunner,
DEBUG_RESIZE,
};
@@ -163,7 +163,7 @@ fn install_keydown(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), J
)
}
#[allow(clippy::needless_pass_by_value)] // So that we can pass it directly to `add_event_listener`
#[expect(clippy::needless_pass_by_value)] // So that we can pass it directly to `add_event_listener`
pub(crate) fn on_keydown(event: web_sys::KeyboardEvent, runner: &mut AppRunner) {
let has_focus = runner.input.raw.focused;
if !has_focus {
@@ -261,7 +261,7 @@ fn install_keyup(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsV
runner_ref.add_event_listener(target, "keyup", on_keyup)
}
#[allow(clippy::needless_pass_by_value)] // So that we can pass it directly to `add_event_listener`
#[expect(clippy::needless_pass_by_value)] // So that we can pass it directly to `add_event_listener`
pub(crate) fn on_keyup(event: web_sys::KeyboardEvent, runner: &mut AppRunner) {
let modifiers = modifiers_from_kb_event(&event);
runner.input.raw.modifiers = modifiers;

View File

@@ -281,7 +281,7 @@ fn create_clipboard_item(mime: &str, bytes: &[u8]) -> Result<web_sys::ClipboardI
let items = js_sys::Object::new();
// SAFETY: I hope so
#[allow(unsafe_code, unused_unsafe)] // Weird false positive
#[expect(unsafe_code, unused_unsafe)] // Weird false positive
unsafe {
js_sys::Reflect::set(&items, &JsValue::from_str(mime), &blob)?
};

View File

@@ -110,7 +110,7 @@ mod console {
/// * `tokio-1.24.1/src/runtime/runtime.rs`
/// * `rerun/src/main.rs`
/// * `core/src/ops/function.rs`
#[allow(dead_code)] // only used on web and in tests
#[allow(dead_code, clippy::allow_attributes)] // only used on web and in tests
fn shorten_file_path(file_path: &str) -> &str {
if let Some(i) = file_path.rfind("/src/") {
if let Some(prev_slash) = file_path[..i].rfind('/') {

View File

@@ -1,7 +1,7 @@
use egui::{Event, UserData, ViewportId};
use egui_glow::glow;
use std::sync::Arc;
use wasm_bindgen::JsCast;
use wasm_bindgen::JsCast as _;
use wasm_bindgen::JsValue;
use web_sys::HtmlCanvasElement;
@@ -27,7 +27,8 @@ impl WebPainterGlow {
) -> Result<Self, String> {
let (gl, shader_prefix) =
init_glow_context_from_canvas(&canvas, options.webgl_context_option)?;
#[allow(clippy::arc_with_non_send_sync)]
#[allow(clippy::arc_with_non_send_sync, clippy::allow_attributes)] // For wasm
let gl = std::sync::Arc::new(gl);
let painter = egui_glow::Painter::new(gl, shader_prefix, None, options.dithering)

View File

@@ -23,7 +23,7 @@ pub(crate) struct WebPainterWgpu {
}
impl WebPainterWgpu {
#[allow(unused)] // only used if `wgpu` is the only active feature.
#[expect(unused)] // only used if `wgpu` is the only active feature.
pub fn render_state(&self) -> Option<RenderState> {
self.render_state.clone()
}
@@ -55,7 +55,7 @@ impl WebPainterWgpu {
})
}
#[allow(unused)] // only used if `wgpu` is the only active feature.
#[expect(unused)] // only used if `wgpu` is the only active feature.
pub async fn new(
ctx: egui::Context,
canvas: web_sys::HtmlCanvasElement,

View File

@@ -37,7 +37,7 @@ pub struct WebRunner {
impl WebRunner {
/// Will install a panic handler that will catch and log any panics
#[allow(clippy::new_without_default)]
#[expect(clippy::new_without_default)]
pub fn new() -> Self {
let panic_handler = PanicHandler::install();
@@ -280,7 +280,7 @@ struct TargetEvent {
closure: Closure<dyn FnMut(web_sys::Event)>,
}
#[allow(unused)]
#[expect(unused)]
struct IntervalHandle {
handle: i32,
closure: Closure<dyn FnMut()>,
@@ -289,7 +289,7 @@ struct IntervalHandle {
enum EventToUnsubscribe {
TargetEvent(TargetEvent),
#[allow(unused)]
#[expect(unused)]
IntervalHandle(IntervalHandle),
}

View File

@@ -9,10 +9,10 @@ authors = [
]
edition.workspace = true
rust-version.workspace = true
homepage = "https://github.com/emilk/egui/tree/master/crates/egui-wgpu"
homepage = "https://github.com/emilk/egui/tree/main/crates/egui-wgpu"
license.workspace = true
readme = "README.md"
repository = "https://github.com/emilk/egui/tree/master/crates/egui-wgpu"
repository = "https://github.com/emilk/egui/tree/main/crates/egui-wgpu"
categories = ["gui", "game-development"]
keywords = ["wgpu", "egui", "gui", "gamedev"]
include = [
@@ -43,7 +43,7 @@ wayland = ["winit?/wayland"]
x11 = ["winit?/x11"]
## Make the renderer `Sync` on wasm, exploiting that by default wasm isn't multithreaded.
## It may make code easier, expecially when targeting both native and web.
## It may make code easier, especially when targeting both native and web.
## On native most wgpu objects are send and sync, on the web they are not (by nature of the WebGPU specification).
## This is not supported in [multithreaded WASM](https://gpuweb.github.io/gpuweb/explainer/#multithreading-transfer).
## Thus that usage is guarded against with compiler errors in wgpu.

View File

@@ -4,6 +4,7 @@ use std::sync::{mpsc, Arc};
use wgpu::{BindGroupLayout, MultisampleState, StoreOp};
/// A texture and a buffer for reading the rendered frame back to the cpu.
///
/// The texture is required since [`wgpu::TextureUsages::COPY_SRC`] is not an allowed
/// flag for the surface texture on all platforms. This means that anytime we want to
/// capture the frame, we first render it to this texture, and then we can copy it to
@@ -125,7 +126,7 @@ impl CaptureState {
// It would be more efficient to reuse the Buffer, e.g. via some kind of ring buffer, but
// for most screenshot use cases this should be fine. When taking many screenshots (e.g. for a video)
// it might make sense to revisit this and implement a more efficient solution.
#[allow(clippy::arc_with_non_send_sync)]
#[allow(clippy::arc_with_non_send_sync, clippy::allow_attributes)] // For wasm
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("egui_screen_capture_buffer"),
size: (self.padding.padded_bytes_per_row * self.texture.height()) as u64,
@@ -184,7 +185,7 @@ impl CaptureState {
tx: CaptureSender,
viewport_id: ViewportId,
) {
#[allow(clippy::arc_with_non_send_sync)]
#[allow(clippy::arc_with_non_send_sync, clippy::allow_attributes)] // For wasm
let buffer = Arc::new(buffer);
let buffer_clone = buffer.clone();
let buffer_slice = buffer_clone.slice(..);
@@ -226,10 +227,10 @@ impl CaptureState {
tx.send((
viewport_id,
data,
ColorImage {
size: [tex_extent.width as usize, tex_extent.height as usize],
ColorImage::new(
[tex_extent.width as usize, tex_extent.height as usize],
pixels,
},
),
))
.ok();
ctx.request_repaint();

View File

@@ -42,8 +42,11 @@ use epaint::mutex::RwLock;
/// An error produced by egui-wgpu.
#[derive(thiserror::Error, Debug)]
pub enum WgpuError {
#[error("Failed to create wgpu adapter, no suitable adapter found: {0}")]
NoSuitableAdapterFound(String),
#[error(transparent)]
RequestAdapterError(#[from] wgpu::RequestAdapterError),
#[error("Adapter selection failed: {0}")]
CustomNativeAdapterSelectionError(String),
#[error("There was no valid format for the surface at all.")]
NoSurfaceFormatsAvailable,
@@ -104,7 +107,7 @@ async fn request_adapter(
force_fallback_adapter: false,
})
.await
.ok_or_else(|| {
.inspect_err(|_err| {
#[cfg(not(target_arch = "wasm32"))]
if _available_adapters.is_empty() {
log::info!("No wgpu adapters found");
@@ -120,8 +123,6 @@ async fn request_adapter(
describe_adapters(_available_adapters)
);
}
WgpuError::NoSuitableAdapterFound("`request_adapters` returned `None`".to_owned())
})?;
#[cfg(target_arch = "wasm32")]
@@ -184,7 +185,6 @@ impl RenderState {
power_preference,
native_adapter_selector: _native_adapter_selector,
device_descriptor,
trace_path,
}) => {
let adapter = {
#[cfg(target_arch = "wasm32")]
@@ -194,7 +194,7 @@ impl RenderState {
#[cfg(not(target_arch = "wasm32"))]
if let Some(native_adapter_selector) = _native_adapter_selector {
native_adapter_selector(&available_adapters, compatible_surface)
.map_err(WgpuError::NoSuitableAdapterFound)
.map_err(WgpuError::CustomNativeAdapterSelectionError)
} else {
request_adapter(
instance,
@@ -209,7 +209,7 @@ impl RenderState {
let (device, queue) = {
profiling::scope!("request_device");
adapter
.request_device(&(*device_descriptor)(&adapter), trace_path.as_deref())
.request_device(&(*device_descriptor)(&adapter))
.await?
};
@@ -242,7 +242,7 @@ impl RenderState {
// On wasm, depending on feature flags, wgpu objects may or may not implement sync.
// It doesn't make sense to switch to Rc for that special usecase, so simply disable the lint.
#[allow(clippy::arc_with_non_send_sync)]
#[allow(clippy::arc_with_non_send_sync, clippy::allow_attributes)] // For wasm
Ok(Self {
adapter,
#[cfg(not(target_arch = "wasm32"))]

View File

@@ -3,7 +3,7 @@
use std::{borrow::Cow, num::NonZeroU64, ops::Range};
use ahash::HashMap;
use epaint::{emath::NumExt, PaintCallbackInfo, Primitive, Vertex};
use epaint::{emath::NumExt as _, PaintCallbackInfo, Primitive, Vertex};
use wgpu::util::DeviceExt as _;
@@ -84,7 +84,7 @@ impl Callback {
///
/// # Example
///
/// See the [`custom3d_wgpu`](https://github.com/emilk/egui/blob/master/crates/egui_demo_app/src/apps/custom3d_wgpu.rs) demo source for a detailed usage example.
/// See the [`custom3d_wgpu`](https://github.com/emilk/egui/blob/main/crates/egui_demo_app/src/apps/custom3d_wgpu.rs) demo source for a detailed usage example.
pub trait CallbackTrait: Send + Sync {
fn prepare(
&self,
@@ -749,7 +749,7 @@ impl Renderer {
///
/// The texture must have the format [`wgpu::TextureFormat::Rgba8UnormSrgb`].
/// Any compare function supplied in the [`wgpu::SamplerDescriptor`] will be ignored.
#[allow(clippy::needless_pass_by_value)] // false positive
#[expect(clippy::needless_pass_by_value)] // false positive
pub fn register_native_texture_with_sampler_options(
&mut self,
device: &wgpu::Device,
@@ -796,7 +796,7 @@ impl Renderer {
/// [`wgpu::SamplerDescriptor`] options.
///
/// This allows applications to reuse [`epaint::TextureId`]s created with custom sampler options.
#[allow(clippy::needless_pass_by_value)] // false positive
#[expect(clippy::needless_pass_by_value)] // false positive
pub fn update_egui_texture_from_wgpu_texture_with_sampler_options(
&mut self,
device: &wgpu::Device,

View File

@@ -48,7 +48,7 @@ impl WgpuSetup {
pub async fn new_instance(&self) -> wgpu::Instance {
match self {
Self::CreateNew(create_new) => {
#[allow(unused_mut)]
#[allow(unused_mut, clippy::allow_attributes)]
let mut backends = create_new.instance_descriptor.backends;
// Don't try WebGPU if we're not in a secure context.
@@ -126,13 +126,6 @@ pub struct WgpuSetupCreateNew {
/// Configuration passed on device request, given an adapter
pub device_descriptor:
Arc<dyn Fn(&wgpu::Adapter) -> wgpu::DeviceDescriptor<'static> + Send + Sync>,
/// Option path to output a wgpu trace file.
///
/// This only works if this feature is enabled in `wgpu-core`.
/// Does not work when running with WebGPU.
/// Defaults to the path set in the `WGPU_TRACE` environment variable.
pub trace_path: Option<std::path::PathBuf>,
}
impl Clone for WgpuSetupCreateNew {
@@ -142,7 +135,6 @@ impl Clone for WgpuSetupCreateNew {
power_preference: self.power_preference,
native_adapter_selector: self.native_adapter_selector.clone(),
device_descriptor: self.device_descriptor.clone(),
trace_path: self.trace_path.clone(),
}
}
}
@@ -156,7 +148,6 @@ impl std::fmt::Debug for WgpuSetupCreateNew {
"native_adapter_selector",
&self.native_adapter_selector.is_some(),
)
.field("trace_path", &self.trace_path)
.finish()
}
}
@@ -195,12 +186,9 @@ impl Default for WgpuSetupCreateNew {
..base_limits
},
memory_hints: wgpu::MemoryHints::default(),
trace: wgpu::Trace::Off,
}
}),
trace_path: std::env::var("WGPU_TRACE")
.ok()
.map(std::path::PathBuf::from),
}
}
}

View File

@@ -575,7 +575,7 @@ impl Painter {
.retain(|id, _| active_viewports.contains(id));
}
#[allow(clippy::needless_pass_by_ref_mut, clippy::unused_self)]
#[expect(clippy::needless_pass_by_ref_mut, clippy::unused_self)]
pub fn destroy(&mut self) {
// TODO(emilk): something here?
}

View File

@@ -5,10 +5,10 @@ authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "Bindings for using egui with winit"
edition.workspace = true
rust-version.workspace = true
homepage = "https://github.com/emilk/egui/tree/master/crates/egui-winit"
homepage = "https://github.com/emilk/egui/tree/main/crates/egui-winit"
license.workspace = true
readme = "README.md"
repository = "https://github.com/emilk/egui/tree/master/crates/egui-winit"
repository = "https://github.com/emilk/egui/tree/main/crates/egui-winit"
categories = ["gui", "game-development"]
keywords = ["winit", "egui", "gui", "gamedev"]
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]

View File

@@ -161,7 +161,7 @@ fn init_smithay_clipboard(
if let Some(RawDisplayHandle::Wayland(display)) = raw_display_handle {
log::trace!("Initializing smithay clipboard…");
#[allow(unsafe_code)]
#[expect(unsafe_code)]
Some(unsafe { smithay_clipboard::Clipboard::new(display.display.as_ptr()) })
} else {
#[cfg(feature = "wayland")]

View File

@@ -856,9 +856,6 @@ impl State {
egui::OutputCommand::OpenUrl(open_url) => {
open_url_in_browser(&open_url.url);
}
egui::OutputCommand::SetPointerPosition(egui::Pos2 { x, y }) => {
let _ = window.set_cursor_position(winit::dpi::LogicalPosition { x, y });
}
}
}
@@ -1147,6 +1144,8 @@ fn key_from_named_key(named_key: winit::keyboard::NamedKey) -> Option<egui::Key>
NamedKey::F33 => Key::F33,
NamedKey::F34 => Key::F34,
NamedKey::F35 => Key::F35,
NamedKey::BrowserBack => Key::BrowserBack,
_ => {
log::trace!("Unknown key: {named_key:?}");
return None;
@@ -1623,6 +1622,7 @@ pub fn create_winit_window_attributes(
title_shown: _title_shown,
titlebar_buttons_shown: _titlebar_buttons_shown,
titlebar_shown: _titlebar_shown,
has_shadow: _has_shadow,
// Windows:
drag_and_drop: _drag_and_drop,
@@ -1767,7 +1767,8 @@ pub fn create_winit_window_attributes(
.with_titlebar_buttons_hidden(!_titlebar_buttons_shown.unwrap_or(true))
.with_titlebar_transparent(!_titlebar_shown.unwrap_or(true))
.with_fullsize_content_view(_fullsize_content_view.unwrap_or(false))
.with_movable_by_window_background(_movable_by_window_background.unwrap_or(false));
.with_movable_by_window_background(_movable_by_window_background.unwrap_or(false))
.with_has_shadow(_has_shadow.unwrap_or(true));
}
window_attributes
@@ -1849,8 +1850,8 @@ pub fn short_device_event_description(event: &winit::event::DeviceEvent) -> &'st
use winit::event::DeviceEvent;
match event {
DeviceEvent::Added { .. } => "DeviceEvent::Added",
DeviceEvent::Removed { .. } => "DeviceEvent::Removed",
DeviceEvent::Added => "DeviceEvent::Added",
DeviceEvent::Removed => "DeviceEvent::Removed",
DeviceEvent::MouseMotion { .. } => "DeviceEvent::MouseMotion",
DeviceEvent::MouseWheel { .. } => "DeviceEvent::MouseWheel",
DeviceEvent::Motion { .. } => "DeviceEvent::Motion",
@@ -1868,11 +1869,11 @@ pub fn short_window_event_description(event: &winit::event::WindowEvent) -> &'st
WindowEvent::ActivationTokenDone { .. } => "WindowEvent::ActivationTokenDone",
WindowEvent::Resized { .. } => "WindowEvent::Resized",
WindowEvent::Moved { .. } => "WindowEvent::Moved",
WindowEvent::CloseRequested { .. } => "WindowEvent::CloseRequested",
WindowEvent::Destroyed { .. } => "WindowEvent::Destroyed",
WindowEvent::CloseRequested => "WindowEvent::CloseRequested",
WindowEvent::Destroyed => "WindowEvent::Destroyed",
WindowEvent::DroppedFile { .. } => "WindowEvent::DroppedFile",
WindowEvent::HoveredFile { .. } => "WindowEvent::HoveredFile",
WindowEvent::HoveredFileCancelled { .. } => "WindowEvent::HoveredFileCancelled",
WindowEvent::HoveredFileCancelled => "WindowEvent::HoveredFileCancelled",
WindowEvent::Focused { .. } => "WindowEvent::Focused",
WindowEvent::KeyboardInput { .. } => "WindowEvent::KeyboardInput",
WindowEvent::ModifiersChanged { .. } => "WindowEvent::ModifiersChanged",
@@ -1883,7 +1884,7 @@ pub fn short_window_event_description(event: &winit::event::WindowEvent) -> &'st
WindowEvent::MouseWheel { .. } => "WindowEvent::MouseWheel",
WindowEvent::MouseInput { .. } => "WindowEvent::MouseInput",
WindowEvent::PinchGesture { .. } => "WindowEvent::PinchGesture",
WindowEvent::RedrawRequested { .. } => "WindowEvent::RedrawRequested",
WindowEvent::RedrawRequested => "WindowEvent::RedrawRequested",
WindowEvent::DoubleTapGesture { .. } => "WindowEvent::DoubleTapGesture",
WindowEvent::RotationGesture { .. } => "WindowEvent::RotationGesture",
WindowEvent::TouchpadPressure { .. } => "WindowEvent::TouchpadPressure",

View File

@@ -87,6 +87,8 @@ ahash.workspace = true
bitflags.workspace = true
nohash-hasher.workspace = true
profiling.workspace = true
smallvec.workspace = true
unicode-segmentation.workspace = true
#! ### Optional dependencies
accesskit = { workspace = true, optional = true }

View File

@@ -1,7 +1,7 @@
There are no stand-alone egui examples, because egui is not stand-alone!
See the top-level [examples](https://github.com/emilk/egui/tree/master/examples/) folder instead.
See the top-level [examples](https://github.com/emilk/egui/tree/main/examples/) folder instead.
There are also plenty of examples in [the online demo](https://www.egui.rs/#demo). You can find the source code for it at <https://github.com/emilk/egui/tree/master/crates/egui_demo_lib>.
There are also plenty of examples in [the online demo](https://www.egui.rs/#demo). You can find the source code for it at <https://github.com/emilk/egui/tree/main/crates/egui_demo_lib>.
To learn how to set up `eframe` for web and native, go to <https://github.com/emilk/eframe_template/> and follow the instructions there!

View File

@@ -0,0 +1,109 @@
use crate::{AtomKind, Id, SizedAtom, Ui};
use emath::{NumExt as _, Vec2};
use epaint::text::TextWrapMode;
/// A low-level ui building block.
///
/// Implements [`From`] for [`String`], [`str`], [`crate::Image`] and much more for convenience.
/// You can directly call the `atom_*` methods on anything that implements `Into<Atom>`.
/// ```
/// # use egui::{Image, emath::Vec2};
/// use egui::AtomExt as _;
/// let string_atom = "Hello".atom_grow(true);
/// let image_atom = Image::new("some_image_url").atom_size(Vec2::splat(20.0));
/// ```
#[derive(Clone, Debug)]
pub struct Atom<'a> {
/// See [`crate::AtomExt::atom_size`]
pub size: Option<Vec2>,
/// See [`crate::AtomExt::atom_max_size`]
pub max_size: Vec2,
/// See [`crate::AtomExt::atom_grow`]
pub grow: bool,
/// See [`crate::AtomExt::atom_shrink`]
pub shrink: bool,
/// The atom type
pub kind: AtomKind<'a>,
}
impl Default for Atom<'_> {
fn default() -> Self {
Atom {
size: None,
max_size: Vec2::INFINITY,
grow: false,
shrink: false,
kind: AtomKind::Empty,
}
}
}
impl<'a> Atom<'a> {
/// Create an empty [`Atom`] marked as `grow`.
///
/// This will expand in size, allowing all preceding atoms to be left-aligned,
/// and all following atoms to be right-aligned
pub fn grow() -> Self {
Atom {
grow: true,
..Default::default()
}
}
/// Create a [`AtomKind::Custom`] with a specific size.
pub fn custom(id: Id, size: impl Into<Vec2>) -> Self {
Atom {
size: Some(size.into()),
kind: AtomKind::Custom(id),
..Default::default()
}
}
/// Turn this into a [`SizedAtom`].
pub fn into_sized(
self,
ui: &Ui,
mut available_size: Vec2,
mut wrap_mode: Option<TextWrapMode>,
) -> SizedAtom<'a> {
if !self.shrink && self.max_size.x.is_infinite() {
wrap_mode = Some(TextWrapMode::Extend);
}
available_size = available_size.at_most(self.max_size);
if let Some(size) = self.size {
available_size = available_size.at_most(size);
}
if self.max_size.x.is_finite() {
wrap_mode = Some(TextWrapMode::Truncate);
}
let (preferred, kind) = self.kind.into_sized(ui, available_size, wrap_mode);
let size = self
.size
.map_or_else(|| kind.size(), |s| s.at_most(self.max_size));
SizedAtom {
size,
preferred_size: preferred,
grow: self.grow,
kind,
}
}
}
impl<'a, T> From<T> for Atom<'a>
where
T: Into<AtomKind<'a>>,
{
fn from(value: T) -> Self {
Atom {
kind: value.into(),
..Default::default()
}
}
}

View File

@@ -0,0 +1,107 @@
use crate::{Atom, FontSelection, Ui};
use emath::Vec2;
/// A trait for conveniently building [`Atom`]s.
///
/// The functions are prefixed with `atom_` to avoid conflicts with e.g. [`crate::RichText::size`].
pub trait AtomExt<'a> {
/// Set the atom to a fixed size.
///
/// If [`Atom::grow`] is `true`, this will be the minimum width.
/// If [`Atom::shrink`] is `true`, this will be the maximum width.
/// If both are true, the width will have no effect.
///
/// [`Self::atom_max_size`] will limit size.
///
/// See [`crate::AtomKind`] docs to see how the size affects the different types.
fn atom_size(self, size: Vec2) -> Atom<'a>;
/// Grow this atom to the available space.
///
/// This will affect the size of the [`Atom`] in the main direction. Since
/// [`crate::AtomLayout`] today only supports horizontal layout, it will affect the width.
///
/// You can also combine this with [`Self::atom_shrink`] to make it always take exactly the
/// remaining space.
fn atom_grow(self, grow: bool) -> Atom<'a>;
/// Shrink this atom if there isn't enough space.
///
/// This will affect the size of the [`Atom`] in the main direction. Since
/// [`crate::AtomLayout`] today only supports horizontal layout, it will affect the width.
///
/// NOTE: Only a single [`Atom`] may shrink for each widget.
///
/// If no atom was set to shrink and `wrap_mode != TextWrapMode::Extend`, the first
/// `AtomKind::Text` is set to shrink.
fn atom_shrink(self, shrink: bool) -> Atom<'a>;
/// Set the maximum size of this atom.
///
/// Will not affect the space taken by `grow` (All atoms marked as grow will always grow
/// equally to fill the available space).
fn atom_max_size(self, max_size: Vec2) -> Atom<'a>;
/// Set the maximum width of this atom.
///
/// Will not affect the space taken by `grow` (All atoms marked as grow will always grow
/// equally to fill the available space).
fn atom_max_width(self, max_width: f32) -> Atom<'a>;
/// Set the maximum height of this atom.
fn atom_max_height(self, max_height: f32) -> Atom<'a>;
/// Set the max height of this atom to match the font size.
///
/// This is useful for e.g. limiting the height of icons in buttons.
fn atom_max_height_font_size(self, ui: &Ui) -> Atom<'a>
where
Self: Sized,
{
let font_selection = FontSelection::default();
let font_id = font_selection.resolve(ui.style());
let height = ui.fonts(|f| f.row_height(&font_id));
self.atom_max_height(height)
}
}
impl<'a, T> AtomExt<'a> for T
where
T: Into<Atom<'a>> + Sized,
{
fn atom_size(self, size: Vec2) -> Atom<'a> {
let mut atom = self.into();
atom.size = Some(size);
atom
}
fn atom_grow(self, grow: bool) -> Atom<'a> {
let mut atom = self.into();
atom.grow = grow;
atom
}
fn atom_shrink(self, shrink: bool) -> Atom<'a> {
let mut atom = self.into();
atom.shrink = shrink;
atom
}
fn atom_max_size(self, max_size: Vec2) -> Atom<'a> {
let mut atom = self.into();
atom.max_size = max_size;
atom
}
fn atom_max_width(self, max_width: f32) -> Atom<'a> {
let mut atom = self.into();
atom.max_size.x = max_width;
atom
}
fn atom_max_height(self, max_height: f32) -> Atom<'a> {
let mut atom = self.into();
atom.max_size.y = max_height;
atom
}
}

View File

@@ -0,0 +1,120 @@
use crate::{Id, Image, ImageSource, SizedAtomKind, TextStyle, Ui, WidgetText};
use emath::Vec2;
use epaint::text::TextWrapMode;
/// The different kinds of [`crate::Atom`]s.
#[derive(Clone, Default, Debug)]
pub enum AtomKind<'a> {
/// Empty, that can be used with [`crate::AtomExt::atom_grow`] to reserve space.
#[default]
Empty,
/// Text atom.
///
/// Truncation within [`crate::AtomLayout`] works like this:
/// -
/// - if `wrap_mode` is not Extend
/// - if no atom is `shrink`
/// - the first text atom is selected and will be marked as `shrink`
/// - the atom marked as `shrink` will shrink / wrap based on the selected wrap mode
/// - any other text atoms will have `wrap_mode` extend
/// - if `wrap_mode` is extend, Text will extend as expected.
///
/// Unless [`crate::AtomExt::atom_max_width`] is set, `wrap_mode` should only be set via [`crate::Style`] or
/// [`crate::AtomLayout::wrap_mode`], as setting a wrap mode on a [`WidgetText`] atom
/// that is not `shrink` will have unexpected results.
///
/// The size is determined by converting the [`WidgetText`] into a galley and using the galleys
/// size. You can use [`crate::AtomExt::atom_size`] to override this, and [`crate::AtomExt::atom_max_width`]
/// to limit the width (Causing the text to wrap or truncate, depending on the `wrap_mode`.
/// [`crate::AtomExt::atom_max_height`] has no effect on text.
Text(WidgetText),
/// Image atom.
///
/// By default the size is determined via [`Image::calc_size`].
/// You can use [`crate::AtomExt::atom_max_size`] or [`crate::AtomExt::atom_size`] to customize the size.
/// There is also a helper [`crate::AtomExt::atom_max_height_font_size`] to set the max height to the
/// default font height, which is convenient for icons.
Image(Image<'a>),
/// For custom rendering.
///
/// You can get the [`crate::Rect`] with the [`Id`] from [`crate::AtomLayoutResponse`] and use a
/// [`crate::Painter`] or [`Ui::put`] to add/draw some custom content.
///
/// Example:
/// ```
/// # use egui::{AtomExt, AtomKind, Atom, Button, Id, __run_test_ui};
/// # use emath::Vec2;
/// # __run_test_ui(|ui| {
/// let id = Id::new("my_button");
/// let response = Button::new(("Hi!", Atom::custom(id, Vec2::splat(18.0)))).atom_ui(ui);
///
/// let rect = response.rect(id);
/// if let Some(rect) = rect {
/// ui.put(rect, Button::new("⏵"));
/// }
/// # });
/// ```
Custom(Id),
}
impl<'a> AtomKind<'a> {
pub fn text(text: impl Into<WidgetText>) -> Self {
AtomKind::Text(text.into())
}
pub fn image(image: impl Into<Image<'a>>) -> Self {
AtomKind::Image(image.into())
}
/// Turn this [`AtomKind`] into a [`SizedAtomKind`].
///
/// This converts [`WidgetText`] into [`crate::Galley`] and tries to load and size [`Image`].
/// The first returned argument is the preferred size.
pub fn into_sized(
self,
ui: &Ui,
available_size: Vec2,
wrap_mode: Option<TextWrapMode>,
) -> (Vec2, SizedAtomKind<'a>) {
match self {
AtomKind::Text(text) => {
let galley = text.into_galley(ui, wrap_mode, available_size.x, TextStyle::Button);
(
galley.size(), // TODO(#5762): calculate the preferred size
SizedAtomKind::Text(galley),
)
}
AtomKind::Image(image) => {
let size = image.load_and_calc_size(ui, available_size);
let size = size.unwrap_or(Vec2::ZERO);
(size, SizedAtomKind::Image(image, size))
}
AtomKind::Custom(id) => (Vec2::ZERO, SizedAtomKind::Custom(id)),
AtomKind::Empty => (Vec2::ZERO, SizedAtomKind::Empty),
}
}
}
impl<'a> From<ImageSource<'a>> for AtomKind<'a> {
fn from(value: ImageSource<'a>) -> Self {
AtomKind::Image(value.into())
}
}
impl<'a> From<Image<'a>> for AtomKind<'a> {
fn from(value: Image<'a>) -> Self {
AtomKind::Image(value)
}
}
impl<T> From<T> for AtomKind<'_>
where
T: Into<WidgetText>,
{
fn from(value: T) -> Self {
AtomKind::Text(value.into())
}
}

View File

@@ -0,0 +1,493 @@
use crate::atomics::ATOMS_SMALL_VEC_SIZE;
use crate::{
AtomKind, Atoms, Frame, Id, Image, IntoAtoms, Response, Sense, SizedAtom, SizedAtomKind, Ui,
Widget,
};
use emath::{Align2, GuiRounding as _, NumExt as _, Rect, Vec2};
use epaint::text::TextWrapMode;
use epaint::{Color32, Galley};
use smallvec::SmallVec;
use std::ops::{Deref, DerefMut};
use std::sync::Arc;
/// Intra-widget layout utility.
///
/// Used to lay out and paint [`crate::Atom`]s.
/// This is used internally by widgets like [`crate::Button`] and [`crate::Checkbox`].
/// You can use it to make your own widgets.
///
/// Painting the atoms can be split in two phases:
/// - [`AtomLayout::allocate`]
/// - calculates sizes
/// - converts texts to [`Galley`]s
/// - allocates a [`Response`]
/// - returns a [`AllocatedAtomLayout`]
/// - [`AllocatedAtomLayout::paint`]
/// - paints the [`Frame`]
/// - calculates individual [`crate::Atom`] positions
/// - paints each single atom
///
/// You can use this to first allocate a response and then modify, e.g., the [`Frame`] on the
/// [`AllocatedAtomLayout`] for interaction styling.
pub struct AtomLayout<'a> {
id: Option<Id>,
pub atoms: Atoms<'a>,
gap: Option<f32>,
pub(crate) frame: Frame,
pub(crate) sense: Sense,
fallback_text_color: Option<Color32>,
min_size: Vec2,
wrap_mode: Option<TextWrapMode>,
align2: Option<Align2>,
}
impl Default for AtomLayout<'_> {
fn default() -> Self {
Self::new(())
}
}
impl<'a> AtomLayout<'a> {
pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
Self {
id: None,
atoms: atoms.into_atoms(),
gap: None,
frame: Frame::default(),
sense: Sense::hover(),
fallback_text_color: None,
min_size: Vec2::ZERO,
wrap_mode: None,
align2: None,
}
}
/// Set the gap between atoms.
///
/// Default: `Spacing::icon_spacing`
#[inline]
pub fn gap(mut self, gap: f32) -> Self {
self.gap = Some(gap);
self
}
/// Set the [`Frame`].
#[inline]
pub fn frame(mut self, frame: Frame) -> Self {
self.frame = frame;
self
}
/// Set the [`Sense`] used when allocating the [`Response`].
#[inline]
pub fn sense(mut self, sense: Sense) -> Self {
self.sense = sense;
self
}
/// Set the fallback (default) text color.
///
/// Default: [`crate::Visuals::text_color`]
#[inline]
pub fn fallback_text_color(mut self, color: Color32) -> Self {
self.fallback_text_color = Some(color);
self
}
/// Set the minimum size of the Widget.
///
/// This will find and expand atoms with `grow: true`.
/// If there are no growable atoms then everything will be left-aligned.
#[inline]
pub fn min_size(mut self, size: Vec2) -> Self {
self.min_size = size;
self
}
/// Set the [`Id`] used to allocate a [`Response`].
#[inline]
pub fn id(mut self, id: Id) -> Self {
self.id = Some(id);
self
}
/// Set the [`TextWrapMode`] for the [`crate::Atom`] marked as `shrink`.
///
/// Only a single [`crate::Atom`] may shrink. If this (or `ui.wrap_mode()`) is not
/// [`TextWrapMode::Extend`] and no item is set to shrink, the first (left-most)
/// [`AtomKind::Text`] will be set to shrink.
#[inline]
pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self {
self.wrap_mode = Some(wrap_mode);
self
}
/// Set the [`Align2`].
///
/// This will align the [`crate::Atom`]s within the [`Rect`] returned by [`Ui::allocate_space`].
///
/// The default is chosen based on the [`Ui`]s [`crate::Layout`]. See
/// [this snapshot](https://github.com/emilk/egui/blob/master/tests/egui_tests/tests/snapshots/layout/button.png)
/// for info on how the [`crate::Layout`] affects the alignment.
#[inline]
pub fn align2(mut self, align2: Align2) -> Self {
self.align2 = Some(align2);
self
}
/// [`AtomLayout::allocate`] and [`AllocatedAtomLayout::paint`] in one go.
pub fn show(self, ui: &mut Ui) -> AtomLayoutResponse {
self.allocate(ui).paint(ui)
}
/// Calculate sizes, create [`Galley`]s and allocate a [`Response`].
///
/// Use the returned [`AllocatedAtomLayout`] for painting.
pub fn allocate(self, ui: &mut Ui) -> AllocatedAtomLayout<'a> {
let Self {
id,
mut atoms,
gap,
frame,
sense,
fallback_text_color,
min_size,
wrap_mode,
align2,
} = self;
let wrap_mode = wrap_mode.unwrap_or(ui.wrap_mode());
// If the TextWrapMode is not Extend, ensure there is some item marked as `shrink`.
// If none is found, mark the first text item as `shrink`.
if wrap_mode != TextWrapMode::Extend {
let any_shrink = atoms.iter().any(|a| a.shrink);
if !any_shrink {
let first_text = atoms
.iter_mut()
.find(|a| matches!(a.kind, AtomKind::Text(..)));
if let Some(atom) = first_text {
atom.shrink = true; // Will make the text truncate or shrink depending on wrap_mode
}
}
}
let id = id.unwrap_or_else(|| ui.next_auto_id());
let fallback_text_color =
fallback_text_color.unwrap_or_else(|| ui.style().visuals.text_color());
let gap = gap.unwrap_or(ui.spacing().icon_spacing);
// The size available for the content
let available_inner_size = ui.available_size() - frame.total_margin().sum();
let mut desired_width = 0.0;
// Preferred 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 height: f32 = 0.0;
let mut sized_items = SmallVec::new();
let mut grow_count = 0;
let mut shrink_item = None;
let align2 = align2.unwrap_or_else(|| {
Align2([ui.layout().horizontal_align(), ui.layout().vertical_align()])
});
if atoms.len() > 1 {
let gap_space = gap * (atoms.len() as f32 - 1.0);
desired_width += gap_space;
preferred_width += gap_space;
}
for (idx, item) in atoms.into_iter().enumerate() {
if item.grow {
grow_count += 1;
}
if item.shrink {
debug_assert!(
shrink_item.is_none(),
"Only one atomic may be marked as shrink. {item:?}"
);
if shrink_item.is_none() {
shrink_item = Some((idx, item));
continue;
}
}
let sized = item.into_sized(ui, available_inner_size, Some(wrap_mode));
let size = sized.size;
desired_width += size.x;
preferred_width += sized.preferred_size.x;
height = height.at_least(size.y);
preferred_height = preferred_height.at_least(sized.preferred_size.y);
sized_items.push(sized);
}
if let Some((index, item)) = shrink_item {
// The `shrink` item gets the remaining space
let available_size_for_shrink_item = Vec2::new(
available_inner_size.x - desired_width,
available_inner_size.y,
);
let sized = item.into_sized(ui, available_size_for_shrink_item, Some(wrap_mode));
let size = sized.size;
desired_width += size.x;
preferred_width += sized.preferred_size.x;
height = height.at_least(size.y);
preferred_height = preferred_height.at_least(sized.preferred_size.y);
sized_items.insert(index, sized);
}
let margin = frame.total_margin();
let desired_size = Vec2::new(desired_width, height);
let frame_size = (desired_size + margin.sum()).at_least(min_size);
let (_, rect) = ui.allocate_space(frame_size);
let mut response = ui.interact(rect, id, sense);
response.intrinsic_size =
Some((Vec2::new(preferred_width, preferred_height) + margin.sum()).at_least(min_size));
AllocatedAtomLayout {
sized_atoms: sized_items,
frame,
fallback_text_color,
response,
grow_count,
desired_size,
align2,
gap,
}
}
}
/// Instructions for painting an [`AtomLayout`].
#[derive(Clone, Debug)]
pub struct AllocatedAtomLayout<'a> {
pub sized_atoms: SmallVec<[SizedAtom<'a>; ATOMS_SMALL_VEC_SIZE]>,
pub frame: Frame,
pub fallback_text_color: Color32,
pub response: Response,
grow_count: usize,
// The size of the inner content, before any growing.
desired_size: Vec2,
align2: Align2,
gap: f32,
}
impl<'atom> AllocatedAtomLayout<'atom> {
pub fn iter_kinds(&self) -> impl Iterator<Item = &SizedAtomKind<'atom>> {
self.sized_atoms.iter().map(|atom| &atom.kind)
}
pub fn iter_kinds_mut(&mut self) -> impl Iterator<Item = &mut SizedAtomKind<'atom>> {
self.sized_atoms.iter_mut().map(|atom| &mut atom.kind)
}
pub fn iter_images(&self) -> impl Iterator<Item = &Image<'atom>> {
self.iter_kinds().filter_map(|kind| {
if let SizedAtomKind::Image(image, _) = kind {
Some(image)
} else {
None
}
})
}
pub fn iter_images_mut(&mut self) -> impl Iterator<Item = &mut Image<'atom>> {
self.iter_kinds_mut().filter_map(|kind| {
if let SizedAtomKind::Image(image, _) = kind {
Some(image)
} else {
None
}
})
}
pub fn iter_texts(&self) -> impl Iterator<Item = &Arc<Galley>> + use<'atom, '_> {
self.iter_kinds().filter_map(|kind| {
if let SizedAtomKind::Text(text) = kind {
Some(text)
} else {
None
}
})
}
pub fn iter_texts_mut(&mut self) -> impl Iterator<Item = &mut Arc<Galley>> + use<'atom, '_> {
self.iter_kinds_mut().filter_map(|kind| {
if let SizedAtomKind::Text(text) = kind {
Some(text)
} else {
None
}
})
}
pub fn map_kind<F>(&mut self, mut f: F)
where
F: FnMut(SizedAtomKind<'atom>) -> SizedAtomKind<'atom>,
{
for kind in self.iter_kinds_mut() {
*kind = f(std::mem::take(kind));
}
}
pub fn map_images<F>(&mut self, mut f: F)
where
F: FnMut(Image<'atom>) -> Image<'atom>,
{
self.map_kind(|kind| {
if let SizedAtomKind::Image(image, size) = kind {
SizedAtomKind::Image(f(image), size)
} else {
kind
}
});
}
/// Paint the [`Frame`] and individual [`crate::Atom`]s.
pub fn paint(self, ui: &Ui) -> AtomLayoutResponse {
let Self {
sized_atoms,
frame,
fallback_text_color,
response,
grow_count,
desired_size,
align2,
gap,
} = self;
let inner_rect = response.rect - self.frame.total_margin();
ui.painter().add(frame.paint(inner_rect));
let width_to_fill = inner_rect.width();
let extra_space = f32::max(width_to_fill - desired_size.x, 0.0);
let grow_width = f32::max(extra_space / grow_count as f32, 0.0).floor_ui();
let aligned_rect = if grow_count > 0 {
align2.align_size_within_rect(Vec2::new(width_to_fill, desired_size.y), inner_rect)
} else {
align2.align_size_within_rect(desired_size, inner_rect)
};
let mut cursor = aligned_rect.left();
let mut response = AtomLayoutResponse::empty(response);
for sized in sized_atoms {
let size = sized.size;
// TODO(lucasmerlin): This is not ideal, since this might lead to accumulated rounding errors
// https://github.com/emilk/egui/pull/5830#discussion_r2079627864
let growth = if sized.is_grow() { grow_width } else { 0.0 };
let frame = aligned_rect
.with_min_x(cursor)
.with_max_x(cursor + size.x + growth);
cursor = frame.right() + gap;
let align = Align2::CENTER_CENTER;
let rect = align.align_size_within_rect(size, frame);
match sized.kind {
SizedAtomKind::Text(galley) => {
ui.painter().galley(rect.min, galley, fallback_text_color);
}
SizedAtomKind::Image(image, _) => {
image.paint_at(ui, rect);
}
SizedAtomKind::Custom(id) => {
debug_assert!(
!response.custom_rects.iter().any(|(i, _)| *i == id),
"Duplicate custom id"
);
response.custom_rects.push((id, rect));
}
SizedAtomKind::Empty => {}
}
}
response
}
}
/// Response from a [`AtomLayout::show`] or [`AllocatedAtomLayout::paint`].
///
/// Use [`AtomLayoutResponse::rect`] to get the response rects from [`AtomKind::Custom`].
#[derive(Clone, Debug)]
pub struct AtomLayoutResponse {
pub response: Response,
// There should rarely be more than one custom rect.
custom_rects: SmallVec<[(Id, Rect); 1]>,
}
impl AtomLayoutResponse {
pub fn empty(response: Response) -> Self {
Self {
response,
custom_rects: Default::default(),
}
}
pub fn custom_rects(&self) -> impl Iterator<Item = (Id, Rect)> + '_ {
self.custom_rects.iter().copied()
}
/// Use this together with [`AtomKind::Custom`] to add custom painting / child widgets.
///
/// NOTE: Don't `unwrap` rects, they might be empty when the widget is not visible.
pub fn rect(&self, id: Id) -> Option<Rect> {
self.custom_rects
.iter()
.find_map(|(i, r)| if *i == id { Some(*r) } else { None })
}
}
impl Widget for AtomLayout<'_> {
fn ui(self, ui: &mut Ui) -> Response {
self.show(ui).response
}
}
impl<'a> Deref for AtomLayout<'a> {
type Target = Atoms<'a>;
fn deref(&self) -> &Self::Target {
&self.atoms
}
}
impl DerefMut for AtomLayout<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.atoms
}
}
impl<'a> Deref for AllocatedAtomLayout<'a> {
type Target = [SizedAtom<'a>];
fn deref(&self) -> &Self::Target {
&self.sized_atoms
}
}
impl DerefMut for AllocatedAtomLayout<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.sized_atoms
}
}

View File

@@ -0,0 +1,259 @@
use crate::{Atom, AtomKind, Image, WidgetText};
use smallvec::SmallVec;
use std::borrow::Cow;
use std::ops::{Deref, DerefMut};
// Rarely there should be more than 2 atoms in one Widget.
// I guess it could happen in a menu button with Image and right text...
pub(crate) const ATOMS_SMALL_VEC_SIZE: usize = 2;
/// A list of [`Atom`]s.
#[derive(Clone, Debug, Default)]
pub struct Atoms<'a>(SmallVec<[Atom<'a>; ATOMS_SMALL_VEC_SIZE]>);
impl<'a> Atoms<'a> {
pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
atoms.into_atoms()
}
/// Insert a new [`Atom`] at the end of the list (right side).
pub fn push_right(&mut self, atom: impl Into<Atom<'a>>) {
self.0.push(atom.into());
}
/// Insert a new [`Atom`] at the beginning of the list (left side).
pub fn push_left(&mut self, atom: impl Into<Atom<'a>>) {
self.0.insert(0, atom.into());
}
/// Concatenate and return the text contents.
// TODO(lucasmerlin): It might not always make sense to return the concatenated text, e.g.
// in a submenu button there is a right text '⏵' which is now passed to the screen reader.
pub fn text(&self) -> Option<Cow<'_, str>> {
let mut string: Option<Cow<'_, str>> = None;
for atom in &self.0 {
if let AtomKind::Text(text) = &atom.kind {
if let Some(string) = &mut string {
let string = string.to_mut();
string.push(' ');
string.push_str(text.text());
} else {
string = Some(Cow::Borrowed(text.text()));
}
}
}
// If there is no text, try to find an image with alt text.
if string.is_none() {
string = self.iter().find_map(|a| match &a.kind {
AtomKind::Image(image) => image.alt_text.as_deref().map(Cow::Borrowed),
_ => None,
});
}
string
}
pub fn iter_kinds(&self) -> impl Iterator<Item = &AtomKind<'a>> {
self.0.iter().map(|atom| &atom.kind)
}
pub fn iter_kinds_mut(&mut self) -> impl Iterator<Item = &mut AtomKind<'a>> {
self.0.iter_mut().map(|atom| &mut atom.kind)
}
pub fn iter_images(&self) -> impl Iterator<Item = &Image<'a>> {
self.iter_kinds().filter_map(|kind| {
if let AtomKind::Image(image) = kind {
Some(image)
} else {
None
}
})
}
pub fn iter_images_mut(&mut self) -> impl Iterator<Item = &mut Image<'a>> {
self.iter_kinds_mut().filter_map(|kind| {
if let AtomKind::Image(image) = kind {
Some(image)
} else {
None
}
})
}
pub fn iter_texts(&self) -> impl Iterator<Item = &WidgetText> + use<'_, 'a> {
self.iter_kinds().filter_map(|kind| {
if let AtomKind::Text(text) = kind {
Some(text)
} else {
None
}
})
}
pub fn iter_texts_mut(&mut self) -> impl Iterator<Item = &mut WidgetText> + use<'a, '_> {
self.iter_kinds_mut().filter_map(|kind| {
if let AtomKind::Text(text) = kind {
Some(text)
} else {
None
}
})
}
pub fn map_atoms(&mut self, mut f: impl FnMut(Atom<'a>) -> Atom<'a>) {
self.iter_mut()
.for_each(|atom| *atom = f(std::mem::take(atom)));
}
pub fn map_kind<F>(&mut self, mut f: F)
where
F: FnMut(AtomKind<'a>) -> AtomKind<'a>,
{
for kind in self.iter_kinds_mut() {
*kind = f(std::mem::take(kind));
}
}
pub fn map_images<F>(&mut self, mut f: F)
where
F: FnMut(Image<'a>) -> Image<'a>,
{
self.map_kind(|kind| {
if let AtomKind::Image(image) = kind {
AtomKind::Image(f(image))
} else {
kind
}
});
}
pub fn map_texts<F>(&mut self, mut f: F)
where
F: FnMut(WidgetText) -> WidgetText,
{
self.map_kind(|kind| {
if let AtomKind::Text(text) = kind {
AtomKind::Text(f(text))
} else {
kind
}
});
}
}
impl<'a> IntoIterator for Atoms<'a> {
type Item = Atom<'a>;
type IntoIter = smallvec::IntoIter<[Atom<'a>; ATOMS_SMALL_VEC_SIZE]>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
/// Helper trait to convert a tuple of atoms into [`Atoms`].
///
/// ```
/// use egui::{Atoms, Image, IntoAtoms, RichText};
/// let atoms: Atoms = (
/// "Some text",
/// RichText::new("Some RichText"),
/// Image::new("some_image_url"),
/// ).into_atoms();
/// ```
impl<'a, T> IntoAtoms<'a> for T
where
T: Into<Atom<'a>>,
{
fn collect(self, atoms: &mut Atoms<'a>) {
atoms.push_right(self);
}
}
/// Trait for turning a tuple of [`Atom`]s into [`Atoms`].
pub trait IntoAtoms<'a> {
fn collect(self, atoms: &mut Atoms<'a>);
fn into_atoms(self) -> Atoms<'a>
where
Self: Sized,
{
let mut atoms = Atoms::default();
self.collect(&mut atoms);
atoms
}
}
impl<'a> IntoAtoms<'a> for Atoms<'a> {
fn collect(self, atoms: &mut Self) {
atoms.0.extend(self.0);
}
}
macro_rules! all_the_atoms {
($($T:ident),*) => {
impl<'a, $($T),*> IntoAtoms<'a> for ($($T),*)
where
$($T: IntoAtoms<'a>),*
{
fn collect(self, _atoms: &mut Atoms<'a>) {
#[allow(clippy::allow_attributes)]
#[allow(non_snake_case)]
let ($($T),*) = self;
$($T.collect(_atoms);)*
}
}
};
}
all_the_atoms!();
all_the_atoms!(T0, T1);
all_the_atoms!(T0, T1, T2);
all_the_atoms!(T0, T1, T2, T3);
all_the_atoms!(T0, T1, T2, T3, T4);
all_the_atoms!(T0, T1, T2, T3, T4, T5);
impl<'a> Deref for Atoms<'a> {
type Target = [Atom<'a>];
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Atoms<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<'a, T: Into<Atom<'a>>> From<Vec<T>> for Atoms<'a> {
fn from(vec: Vec<T>) -> Self {
Atoms(vec.into_iter().map(Into::into).collect())
}
}
impl<'a, T: Into<Atom<'a>> + Clone> From<&[T]> for Atoms<'a> {
fn from(slice: &[T]) -> Self {
Atoms(slice.iter().cloned().map(Into::into).collect())
}
}
impl<'a, Item: Into<Atom<'a>>> FromIterator<Item> for Atoms<'a> {
fn from_iter<T: IntoIterator<Item = Item>>(iter: T) -> Self {
Atoms(iter.into_iter().map(Into::into).collect())
}
}
#[cfg(test)]
mod tests {
use crate::Atoms;
#[test]
fn collect_atoms() {
let _: Atoms<'_> = ["Hello", "World"].into_iter().collect();
let _ = Atoms::from(vec!["Hi"]);
let _ = Atoms::from(["Hi"].as_slice());
}
}

View File

@@ -0,0 +1,15 @@
mod atom;
mod atom_ext;
mod atom_kind;
mod atom_layout;
mod atoms;
mod sized_atom;
mod sized_atom_kind;
pub use atom::*;
pub use atom_ext::*;
pub use atom_kind::*;
pub use atom_layout::*;
pub use atoms::*;
pub use sized_atom::*;
pub use sized_atom_kind::*;

View File

@@ -0,0 +1,26 @@
use crate::SizedAtomKind;
use emath::Vec2;
/// A [`crate::Atom`] which has been sized.
#[derive(Clone, Debug)]
pub struct SizedAtom<'a> {
pub(crate) grow: bool,
/// The size of the atom.
///
/// Used for placing this atom in [`crate::AtomLayout`], the cursor will advance by
/// size.x + gap.
pub size: Vec2,
/// Preferred size of the atom. This is used to calculate `Response::intrinsic_size`.
pub preferred_size: Vec2,
pub kind: SizedAtomKind<'a>,
}
impl SizedAtom<'_> {
/// Was this [`crate::Atom`] marked as `grow`?
pub fn is_grow(&self) -> bool {
self.grow
}
}

View File

@@ -0,0 +1,25 @@
use crate::{Id, Image};
use emath::Vec2;
use epaint::Galley;
use std::sync::Arc;
/// A sized [`crate::AtomKind`].
#[derive(Clone, Default, Debug)]
pub enum SizedAtomKind<'a> {
#[default]
Empty,
Text(Arc<Galley>),
Image(Image<'a>, Vec2),
Custom(Id),
}
impl SizedAtomKind<'_> {
/// Get the calculated size.
pub fn size(&self) -> Vec2 {
match self {
SizedAtomKind::Text(galley) => galley.size(),
SizedAtomKind::Image(_, size) => *size,
SizedAtomKind::Empty | SizedAtomKind::Custom(_) => Vec2::ZERO,
}
}
}

View File

@@ -1,5 +1,5 @@
/// A cache, storing some value for some length of time.
#[allow(clippy::len_without_is_empty)]
#[expect(clippy::len_without_is_empty)]
pub trait CacheTrait: 'static + Send + Sync {
/// Call once per frame to evict cache.
fn update(&mut self);

View File

@@ -5,8 +5,8 @@
use emath::GuiRounding as _;
use crate::{
emath, pos2, Align2, Context, Id, InnerResponse, LayerId, Layout, NumExt, Order, Pos2, Rect,
Response, Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetRect, WidgetWithState,
emath, pos2, Align2, Context, Id, InnerResponse, LayerId, Layout, NumExt as _, Order, Pos2,
Rect, Response, Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetRect, WidgetWithState,
};
/// State of an [`Area`] that is persisted between frames.
@@ -602,7 +602,7 @@ impl Prepared {
self.move_response.id
}
#[allow(clippy::needless_pass_by_value)] // intentional to swallow up `content_ui`.
#[expect(clippy::needless_pass_by_value)] // intentional to swallow up `content_ui`.
pub(crate) fn end(self, ctx: &Context, content_ui: Ui) -> Response {
let Self {
info: _,

View File

@@ -1,7 +1,7 @@
use std::hash::Hash;
use crate::{
emath, epaint, pos2, remap, remap_clamp, vec2, Context, Id, InnerResponse, NumExt, Rect,
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,
};

View File

@@ -2,11 +2,11 @@ use epaint::Shape;
use crate::{
epaint, style::StyleModifier, style::WidgetVisuals, vec2, Align2, Context, Id, InnerResponse,
NumExt, Painter, Popup, PopupCloseBehavior, Rect, Response, ScrollArea, Sense, Stroke,
NumExt as _, Painter, Popup, PopupCloseBehavior, Rect, Response, ScrollArea, Sense, Stroke,
TextStyle, TextWrapMode, Ui, UiBuilder, Vec2, WidgetInfo, WidgetText, WidgetType,
};
#[allow(unused_imports)] // Documentation
#[expect(unused_imports)] // Documentation
use crate::style::Spacing;
/// A function that paints the [`ComboBox`] icon
@@ -16,9 +16,10 @@ pub type IconPainter = Box<dyn FnOnce(&Ui, Rect, &WidgetVisuals, bool)>;
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// # #[derive(Debug, PartialEq)]
/// # #[derive(Debug, PartialEq, Copy, Clone)]
/// # enum Enum { First, Second, Third }
/// # let mut selected = Enum::First;
/// let before = selected;
/// egui::ComboBox::from_label("Select one!")
/// .selected_text(format!("{:?}", selected))
/// .show_ui(ui, |ui| {
@@ -27,6 +28,10 @@ pub type IconPainter = Box<dyn FnOnce(&Ui, Rect, &WidgetVisuals, bool)>;
/// ui.selectable_value(&mut selected, Enum::Third, "Third");
/// }
/// );
///
/// if selected != before {
/// // Handle selection change
/// }
/// # });
/// ```
#[must_use = "You should call .show*"]
@@ -86,7 +91,7 @@ impl ComboBox {
}
/// Without label.
#[deprecated = "Renamed id_salt"]
#[deprecated = "Renamed from_id_salt"]
pub fn from_id_source(id_salt: impl std::hash::Hash) -> Self {
Self::from_id_salt(id_salt)
}
@@ -297,7 +302,7 @@ impl ComboBox {
}
}
#[allow(clippy::too_many_arguments)]
#[expect(clippy::too_many_arguments)]
fn combo_box_dyn<'c, R>(
ui: &mut Ui,
button_id: Id,

View File

@@ -1,7 +1,7 @@
use crate::style::StyleModifier;
use crate::{
Button, Color32, Context, Frame, Id, InnerResponse, Layout, Popup, PopupCloseBehavior,
Response, Style, Ui, UiBuilder, UiKind, UiStack, UiStackInfo, Widget, WidgetText,
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 epaint::Stroke;
@@ -159,6 +159,7 @@ impl MenuState {
}
/// Horizontal menu bar where you can add [`MenuButton`]s.
///
/// 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`].
@@ -242,8 +243,8 @@ pub struct MenuButton<'a> {
}
impl<'a> MenuButton<'a> {
pub fn new(text: impl Into<WidgetText>) -> Self {
Self::from_button(Button::new(text))
pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
Self::from_button(Button::new(atoms.into_atoms()))
}
/// Set the config for the menu.
@@ -292,8 +293,8 @@ impl<'a> SubMenuButton<'a> {
/// The default right arrow symbol: `"⏵"`
pub const RIGHT_ARROW: &'static str = "";
pub fn new(text: impl Into<WidgetText>) -> Self {
Self::from_button(Button::new(text).right_text(""))
pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
Self::from_button(Button::new(atoms.into_atoms()).right_text(""))
}
/// Create a new submenu button from a [`Button`].

View File

@@ -29,7 +29,7 @@ pub use {
panel::{CentralPanel, SidePanel, TopBottomPanel},
popup::*,
resize::Resize,
scene::Scene,
scene::{DragPanButtons, Scene},
scroll_area::ScrollArea,
sides::Sides,
tooltip::*,

View File

@@ -4,7 +4,7 @@
use crate::containers::tooltip::Tooltip;
use crate::{
Align, Context, Id, LayerId, Layout, Popup, PopupAnchor, PopupCloseBehavior, Pos2, Rect,
Response, Ui, Widget, WidgetText,
Response, Ui, Widget as _, WidgetText,
};
use emath::RectAlign;
// ----------------------------------------------------------------------------
@@ -19,7 +19,7 @@ use emath::RectAlign;
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// # #[allow(deprecated)]
/// # #[expect(deprecated)]
/// if ui.ui_contains_pointer() {
/// egui::show_tooltip(ui.ctx(), ui.layer_id(), egui::Id::new("my_tooltip"), |ui| {
/// ui.label("Helpful text");
@@ -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(widget_id, ctx.clone(), PopupAnchor::Pointer, parent_layer)
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(widget_id, ctx.clone(), *widget_rect, parent_layer)
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(widget_id, ctx.clone(), suggested_position, parent_layer)
Tooltip::always_open(ctx.clone(), parent_layer, widget_id, suggested_position)
.show(add_contents)
.map(|response| response.inner)
}
@@ -177,7 +177,7 @@ pub fn popup_below_widget<R>(
/// }
/// let below = egui::AboveOrBelow::Below;
/// let close_on_click_outside = egui::PopupCloseBehavior::CloseOnClickOutside;
/// # #[allow(deprecated)]
/// # #[expect(deprecated)]
/// egui::popup_above_or_below_widget(ui, popup_id, &response, below, close_on_click_outside, |ui| {
/// ui.set_min_width(200.0); // if you want to control the size
/// ui.label("Some more info, or things you can select:");

View File

@@ -18,7 +18,7 @@
use emath::GuiRounding as _;
use crate::{
lerp, vec2, Align, Context, CursorIcon, Frame, Id, InnerResponse, LayerId, Layout, NumExt,
lerp, vec2, Align, Context, CursorIcon, Frame, Id, InnerResponse, LayerId, Layout, NumExt as _,
Rangef, Rect, Sense, Stroke, Ui, UiBuilder, UiKind, UiStackInfo, Vec2,
};

View File

@@ -466,7 +466,7 @@ impl<'a> Popup<'a> {
};
RectAlign::find_best_align(
#[allow(clippy::iter_on_empty_collections)]
#[expect(clippy::iter_on_empty_collections)]
once(self.rect_align).chain(
self.alternative_aligns
// Need the empty slice so the iters have the same type so we can unwrap_or

View File

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

View File

@@ -1,9 +1,10 @@
use core::f32;
use emath::{GuiRounding, Pos2};
use emath::{GuiRounding as _, Pos2};
use crate::{
emath::TSTransform, InnerResponse, LayerId, Rangef, Rect, Response, Sense, Ui, UiBuilder, Vec2,
emath::TSTransform, InnerResponse, LayerId, PointerButton, Rangef, Rect, Response, Sense, Ui,
UiBuilder, Vec2,
};
/// Creates a transformation that fits a given scene rectangle into the available screen size.
@@ -44,14 +45,41 @@ fn fit_to_rect_in_scene(
#[must_use = "You should call .show()"]
pub struct Scene {
zoom_range: Rangef,
sense: Sense,
max_inner_size: Vec2,
drag_pan_buttons: DragPanButtons,
}
/// Specifies which pointer buttons can be used to pan the scene by dragging.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct DragPanButtons(u8);
bitflags::bitflags! {
impl DragPanButtons: u8 {
/// [PointerButton::Primary]
const PRIMARY = 1 << 0;
/// [PointerButton::Secondary]
const SECONDARY = 1 << 1;
/// [PointerButton::Middle]
const MIDDLE = 1 << 2;
/// [PointerButton::Extra1]
const EXTRA_1 = 1 << 3;
/// [PointerButton::Extra2]
const EXTRA_2 = 1 << 4;
}
}
impl Default for Scene {
fn default() -> Self {
Self {
zoom_range: Rangef::new(f32::EPSILON, 1.0),
sense: Sense::click_and_drag(),
max_inner_size: Vec2::splat(1000.0),
drag_pan_buttons: DragPanButtons::all(),
}
}
}
@@ -62,6 +90,17 @@ impl Scene {
Default::default()
}
/// Specify what type of input the scene should respond to.
///
/// The default is `Sense::click_and_drag()`.
///
/// Set this to `Sense::hover()` to disable panning via clicking and dragging.
#[inline]
pub fn sense(mut self, sense: Sense) -> Self {
self.sense = sense;
self
}
/// Set the allowed zoom range.
///
/// The default zoom range is `0.0..=1.0`,
@@ -82,6 +121,15 @@ impl Scene {
self
}
/// Specify which pointer buttons can be used to pan by clicking and dragging.
///
/// By default, this is `DragPanButtons::all()`.
#[inline]
pub fn drag_pan_buttons(mut self, flags: DragPanButtons) -> Self {
self.drag_pan_buttons = flags;
self
}
/// `scene_rect` contains the view bounds of the inner [`Ui`].
///
/// `scene_rect` will be mutated by any panning/zooming done by the user.
@@ -149,7 +197,7 @@ impl Scene {
UiBuilder::new()
.layer_id(scene_layer_id)
.max_rect(Rect::from_min_size(Pos2::ZERO, self.max_inner_size))
.sense(Sense::click_and_drag()),
.sense(self.sense),
);
let mut pan_response = local_ui.response();
@@ -179,7 +227,15 @@ impl Scene {
/// Helper function to handle pan and zoom interactions on a response.
pub fn register_pan_and_zoom(&self, ui: &Ui, resp: &mut Response, to_global: &mut TSTransform) {
if resp.dragged() {
let dragged = self.drag_pan_buttons.iter().any(|button| match button {
DragPanButtons::PRIMARY => resp.dragged_by(PointerButton::Primary),
DragPanButtons::SECONDARY => resp.dragged_by(PointerButton::Secondary),
DragPanButtons::MIDDLE => resp.dragged_by(PointerButton::Middle),
DragPanButtons::EXTRA_1 => resp.dragged_by(PointerButton::Extra1),
DragPanButtons::EXTRA_2 => resp.dragged_by(PointerButton::Extra2),
_ => false,
});
if dragged {
to_global.translation += to_global.scaling * resp.drag_delta();
resp.mark_changed();
}

View File

@@ -1,8 +1,10 @@
#![allow(clippy::needless_range_loop)]
use std::ops::{Add, AddAssign, BitOr, BitOrAssign};
use crate::{
emath, epaint, lerp, pass_state, pos2, remap, remap_clamp, Context, Id, NumExt, Pos2, Rangef,
Rect, Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b,
emath, epaint, lerp, pass_state, pos2, remap, remap_clamp, Context, CursorIcon, Id,
NumExt as _, Pos2, Rangef, Rect, Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b,
};
#[derive(Clone, Copy, Debug)]
@@ -133,6 +135,113 @@ impl ScrollBarVisibility {
];
}
/// What is the source of scrolling for a [`ScrollArea`].
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct ScrollSource {
/// Scroll the area by dragging a scroll bar.
///
/// By default the scroll bars remain visible to show current position.
/// To hide them use [`ScrollArea::scroll_bar_visibility()`].
pub scroll_bar: bool,
/// Scroll the area by dragging the contents.
pub drag: bool,
/// Scroll the area by scrolling (or shift scrolling) the mouse wheel with
/// the mouse cursor over the [`ScrollArea`].
pub mouse_wheel: bool,
}
impl Default for ScrollSource {
fn default() -> Self {
Self::ALL
}
}
impl ScrollSource {
pub const NONE: Self = Self {
scroll_bar: false,
drag: false,
mouse_wheel: false,
};
pub const ALL: Self = Self {
scroll_bar: true,
drag: true,
mouse_wheel: true,
};
pub const SCROLL_BAR: Self = Self {
scroll_bar: true,
drag: false,
mouse_wheel: false,
};
pub const DRAG: Self = Self {
scroll_bar: false,
drag: true,
mouse_wheel: false,
};
pub const MOUSE_WHEEL: Self = Self {
scroll_bar: false,
drag: false,
mouse_wheel: true,
};
/// Is everything disabled?
#[inline]
pub fn is_none(&self) -> bool {
self == &Self::NONE
}
/// Is anything enabled?
#[inline]
pub fn any(&self) -> bool {
self.scroll_bar | self.drag | self.mouse_wheel
}
/// Is everything enabled?
#[inline]
pub fn is_all(&self) -> bool {
self.scroll_bar & self.drag & self.mouse_wheel
}
}
impl BitOr for ScrollSource {
type Output = Self;
#[inline]
fn bitor(self, rhs: Self) -> Self::Output {
Self {
scroll_bar: self.scroll_bar | rhs.scroll_bar,
drag: self.drag | rhs.drag,
mouse_wheel: self.mouse_wheel | rhs.mouse_wheel,
}
}
}
#[expect(clippy::suspicious_arithmetic_impl)]
impl Add for ScrollSource {
type Output = Self;
#[inline]
fn add(self, rhs: Self) -> Self::Output {
self | rhs
}
}
impl BitOrAssign for ScrollSource {
#[inline]
fn bitor_assign(&mut self, rhs: Self) {
*self = *self | rhs;
}
}
impl AddAssign for ScrollSource {
#[inline]
fn add_assign(&mut self, rhs: Self) {
*self = *self + rhs;
}
}
/// Add vertical and/or horizontal scrolling to a contained [`Ui`].
///
/// By default, scroll bars only show up when needed, i.e. when the contents
@@ -168,7 +277,7 @@ impl ScrollBarVisibility {
#[must_use = "You should call .show()"]
pub struct ScrollArea {
/// Do we have horizontal/vertical scrolling enabled?
scroll_enabled: Vec2b,
direction_enabled: Vec2b,
auto_shrink: Vec2b,
max_size: Vec2,
@@ -178,10 +287,10 @@ pub struct ScrollArea {
id_salt: Option<Id>,
offset_x: Option<f32>,
offset_y: Option<f32>,
/// If false, we ignore scroll events.
scrolling_enabled: bool,
drag_to_scroll: bool,
on_hover_cursor: Option<CursorIcon>,
on_drag_cursor: Option<CursorIcon>,
scroll_source: ScrollSource,
wheel_scroll_multiplier: Vec2,
/// If true for vertical or horizontal the scroll wheel will stick to the
/// end position until user manually changes position. It will become true
@@ -220,9 +329,9 @@ impl ScrollArea {
/// Create a scroll area where you decide which axis has scrolling enabled.
/// For instance, `ScrollArea::new([true, false])` enables horizontal scrolling.
pub fn new(scroll_enabled: impl Into<Vec2b>) -> Self {
pub fn new(direction_enabled: impl Into<Vec2b>) -> Self {
Self {
scroll_enabled: scroll_enabled.into(),
direction_enabled: direction_enabled.into(),
auto_shrink: Vec2b::TRUE,
max_size: Vec2::INFINITY,
min_scrolled_size: Vec2::splat(64.0),
@@ -231,8 +340,10 @@ impl ScrollArea {
id_salt: None,
offset_x: None,
offset_y: None,
scrolling_enabled: true,
drag_to_scroll: true,
on_hover_cursor: None,
on_drag_cursor: None,
scroll_source: ScrollSource::default(),
wheel_scroll_multiplier: Vec2::splat(1.0),
stick_to_end: Vec2b::FALSE,
animated: true,
}
@@ -355,17 +466,41 @@ impl ScrollArea {
self
}
/// Set the cursor used when the mouse pointer is hovering over the [`ScrollArea`].
///
/// Only applies if [`Self::scroll_source()`] has set [`ScrollSource::drag`] to `true`.
///
/// Any changes to the mouse cursor made within the contents of the [`ScrollArea`] will
/// override this setting.
#[inline]
pub fn on_hover_cursor(mut self, cursor: CursorIcon) -> Self {
self.on_hover_cursor = Some(cursor);
self
}
/// Set the cursor used when the [`ScrollArea`] is being dragged.
///
/// Only applies if [`Self::scroll_source()`] has set [`ScrollSource::drag`] to `true`.
///
/// Any changes to the mouse cursor made within the contents of the [`ScrollArea`] will
/// override this setting.
#[inline]
pub fn on_drag_cursor(mut self, cursor: CursorIcon) -> Self {
self.on_drag_cursor = Some(cursor);
self
}
/// Turn on/off scrolling on the horizontal axis.
#[inline]
pub fn hscroll(mut self, hscroll: bool) -> Self {
self.scroll_enabled[0] = hscroll;
self.direction_enabled[0] = hscroll;
self
}
/// Turn on/off scrolling on the vertical axis.
#[inline]
pub fn vscroll(mut self, vscroll: bool) -> Self {
self.scroll_enabled[1] = vscroll;
self.direction_enabled[1] = vscroll;
self
}
@@ -373,16 +508,8 @@ impl ScrollArea {
///
/// You can pass in `false`, `true`, `[false, true]` etc.
#[inline]
pub fn scroll(mut self, scroll_enabled: impl Into<Vec2b>) -> Self {
self.scroll_enabled = scroll_enabled.into();
self
}
/// Turn on/off scrolling on the horizontal/vertical axes.
#[deprecated = "Renamed to `scroll`"]
#[inline]
pub fn scroll2(mut self, scroll_enabled: impl Into<Vec2b>) -> Self {
self.scroll_enabled = scroll_enabled.into();
pub fn scroll(mut self, direction_enabled: impl Into<Vec2b>) -> Self {
self.direction_enabled = direction_enabled.into();
self
}
@@ -395,9 +522,14 @@ impl ScrollArea {
/// is typing text in a [`crate::TextEdit`] widget contained within the scroll area.
///
/// This controls both scrolling directions.
#[deprecated = "Use `ScrollArea::scroll_source()"]
#[inline]
pub fn enable_scrolling(mut self, enable: bool) -> Self {
self.scrolling_enabled = enable;
self.scroll_source = if enable {
ScrollSource::ALL
} else {
ScrollSource::NONE
};
self
}
@@ -408,9 +540,28 @@ impl ScrollArea {
/// If `true`, the [`ScrollArea`] will sense drags.
///
/// Default: `true`.
#[deprecated = "Use `ScrollArea::scroll_source()"]
#[inline]
pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self {
self.drag_to_scroll = drag_to_scroll;
self.scroll_source.drag = drag_to_scroll;
self
}
/// What sources does the [`ScrollArea`] use for scrolling the contents.
#[inline]
pub fn scroll_source(mut self, scroll_source: ScrollSource) -> Self {
self.scroll_source = scroll_source;
self
}
/// The scroll amount caused by a mouse wheel scroll is multiplied by this amount.
///
/// Independent for each scroll direction. Defaults to `Vec2{x: 1.0, y: 1.0}`.
///
/// This can invert or effectively disable mouse scrolling.
#[inline]
pub fn wheel_scroll_multiplier(mut self, multiplier: Vec2) -> Self {
self.wheel_scroll_multiplier = multiplier;
self
}
@@ -437,7 +588,7 @@ impl ScrollArea {
/// Is any scrolling enabled?
pub(crate) fn is_any_scroll_enabled(&self) -> bool {
self.scroll_enabled[0] || self.scroll_enabled[1]
self.direction_enabled[0] || self.direction_enabled[1]
}
/// The scroll handle will stick to the rightmost position even while the content size
@@ -472,7 +623,7 @@ struct Prepared {
auto_shrink: Vec2b,
/// Does this `ScrollArea` have horizontal/vertical scrolling enabled?
scroll_enabled: Vec2b,
direction_enabled: Vec2b,
/// Smoothly interpolated boolean of whether or not to show the scroll bars.
show_bars_factor: Vec2,
@@ -500,7 +651,8 @@ struct Prepared {
/// `viewport.min == ZERO` means we scrolled to the top.
viewport: Rect,
scrolling_enabled: bool,
scroll_source: ScrollSource,
wheel_scroll_multiplier: Vec2,
stick_to_end: Vec2b,
/// If there was a scroll target before the [`ScrollArea`] was added this frame, it's
@@ -513,7 +665,7 @@ struct Prepared {
impl ScrollArea {
fn begin(self, ui: &mut Ui) -> Prepared {
let Self {
scroll_enabled,
direction_enabled,
auto_shrink,
max_size,
min_scrolled_size,
@@ -522,14 +674,15 @@ impl ScrollArea {
id_salt,
offset_x,
offset_y,
scrolling_enabled,
drag_to_scroll,
on_hover_cursor,
on_drag_cursor,
scroll_source,
wheel_scroll_multiplier,
stick_to_end,
animated,
} = self;
let ctx = ui.ctx().clone();
let scrolling_enabled = scrolling_enabled && ui.is_enabled();
let id_salt = id_salt.unwrap_or_else(|| Id::new("scroll_area"));
let id = ui.make_persistent_id(id_salt);
@@ -546,7 +699,7 @@ impl ScrollArea {
let show_bars: Vec2b = match scroll_bar_visibility {
ScrollBarVisibility::AlwaysHidden => Vec2b::FALSE,
ScrollBarVisibility::VisibleWhenNeeded => state.show_scroll,
ScrollBarVisibility::AlwaysVisible => scroll_enabled,
ScrollBarVisibility::AlwaysVisible => direction_enabled,
};
let show_bars_factor = Vec2::new(
@@ -568,7 +721,7 @@ impl ScrollArea {
// one shouldn't collapse into nothingness.
// See https://github.com/emilk/egui/issues/1097
for d in 0..2 {
if scroll_enabled[d] {
if direction_enabled[d] {
inner_size[d] = inner_size[d].max(min_scrolled_size[d]);
}
}
@@ -585,7 +738,7 @@ impl ScrollArea {
} else {
// Tell the inner Ui to use as much space as possible, we can scroll to see it!
for d in 0..2 {
if scroll_enabled[d] {
if direction_enabled[d] {
content_max_size[d] = f32::INFINITY;
}
}
@@ -603,7 +756,7 @@ impl ScrollArea {
let clip_rect_margin = ui.visuals().clip_rect_margin;
let mut content_clip_rect = ui.clip_rect();
for d in 0..2 {
if scroll_enabled[d] {
if direction_enabled[d] {
content_clip_rect.min[d] = inner_rect.min[d] - clip_rect_margin;
content_clip_rect.max[d] = inner_rect.max[d] + clip_rect_margin;
} else {
@@ -619,7 +772,8 @@ impl ScrollArea {
let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size);
let dt = ui.input(|i| i.stable_dt).at_most(0.1);
if (scrolling_enabled && drag_to_scroll)
if scroll_source.drag
&& ui.is_enabled()
&& (state.content_is_too_large[0] || state.content_is_too_large[1])
{
// Drag contents to scroll (for touch screens mostly).
@@ -634,7 +788,7 @@ impl ScrollArea {
.is_some_and(|response| response.dragged())
{
for d in 0..2 {
if scroll_enabled[d] {
if direction_enabled[d] {
ui.input(|input| {
state.offset[d] -= input.pointer.delta()[d];
});
@@ -649,7 +803,7 @@ impl ScrollArea {
.is_some_and(|response| response.drag_stopped())
{
state.vel =
scroll_enabled.to_vec2() * ui.input(|input| input.pointer.velocity());
direction_enabled.to_vec2() * ui.input(|input| input.pointer.velocity());
}
for d in 0..2 {
// Kinetic scrolling
@@ -668,6 +822,19 @@ impl ScrollArea {
}
}
}
// Set the desired mouse cursors.
if let Some(response) = content_response_option {
if response.dragged() {
if let Some(cursor) = on_drag_cursor {
response.on_hover_cursor(cursor);
}
} else if response.hovered() {
if let Some(cursor) = on_hover_cursor {
response.on_hover_cursor(cursor);
}
}
}
}
// Scroll with an animation if we have a target offset (that hasn't been cleared by the code
@@ -709,7 +876,7 @@ impl ScrollArea {
id,
state,
auto_shrink,
scroll_enabled,
direction_enabled,
show_bars_factor,
current_bar_use,
scroll_bar_visibility,
@@ -717,7 +884,8 @@ impl ScrollArea {
inner_rect,
content_ui,
viewport,
scrolling_enabled,
scroll_source,
wheel_scroll_multiplier,
stick_to_end,
saved_scroll_target,
animated,
@@ -824,14 +992,15 @@ impl Prepared {
mut state,
inner_rect,
auto_shrink,
scroll_enabled,
direction_enabled,
mut show_bars_factor,
current_bar_use,
scroll_bar_visibility,
scroll_bar_rect,
content_ui,
viewport: _,
scrolling_enabled,
scroll_source,
wheel_scroll_multiplier,
stick_to_end,
saved_scroll_target,
animated,
@@ -854,7 +1023,7 @@ impl Prepared {
.ctx()
.pass_state_mut(|state| state.scroll_target[d].take());
if scroll_enabled[d] {
if direction_enabled[d] {
if let Some(target) = scroll_target {
let pass_state::ScrollTarget {
range,
@@ -930,7 +1099,7 @@ impl Prepared {
let mut inner_size = inner_rect.size();
for d in 0..2 {
inner_size[d] = match (scroll_enabled[d], auto_shrink[d]) {
inner_size[d] = match (direction_enabled[d], auto_shrink[d]) {
(true, true) => inner_size[d].min(content_size[d]), // shrink scroll area if content is small
(true, false) => inner_size[d], // let scroll area be larger than content; fill with blank space
(false, true) => content_size[d], // Follow the content (expand/contract to fit it).
@@ -944,18 +1113,18 @@ impl Prepared {
let outer_rect = Rect::from_min_size(inner_rect.min, inner_rect.size() + current_bar_use);
let content_is_too_large = Vec2b::new(
scroll_enabled[0] && inner_rect.width() < content_size.x,
scroll_enabled[1] && inner_rect.height() < content_size.y,
direction_enabled[0] && inner_rect.width() < content_size.x,
direction_enabled[1] && inner_rect.height() < content_size.y,
);
let max_offset = content_size - inner_rect.size();
let is_hovering_outer_rect = ui.rect_contains_pointer(outer_rect);
if scrolling_enabled && is_hovering_outer_rect {
if scroll_source.mouse_wheel && ui.is_enabled() && is_hovering_outer_rect {
let always_scroll_enabled_direction = ui.style().always_scroll_the_only_direction
&& scroll_enabled[0] != scroll_enabled[1];
&& direction_enabled[0] != direction_enabled[1];
for d in 0..2 {
if scroll_enabled[d] {
let scroll_delta = ui.ctx().input_mut(|input| {
if direction_enabled[d] {
let scroll_delta = ui.ctx().input(|input| {
if always_scroll_enabled_direction {
// no bidirectional scrolling; allow horizontal scrolling without pressing shift
input.smooth_scroll_delta[0] + input.smooth_scroll_delta[1]
@@ -963,6 +1132,7 @@ impl Prepared {
input.smooth_scroll_delta[d]
}
});
let scroll_delta = scroll_delta * wheel_scroll_multiplier[d];
let scrolling_up = state.offset[d] > 0.0 && scroll_delta > 0.0;
let scrolling_down = state.offset[d] < max_offset[d] && scroll_delta < 0.0;
@@ -990,7 +1160,7 @@ impl Prepared {
let show_scroll_this_frame = match scroll_bar_visibility {
ScrollBarVisibility::AlwaysHidden => Vec2b::FALSE,
ScrollBarVisibility::VisibleWhenNeeded => content_is_too_large,
ScrollBarVisibility::AlwaysVisible => scroll_enabled,
ScrollBarVisibility::AlwaysVisible => direction_enabled,
};
// Avoid frame delay; start showing scroll bar right away:
@@ -1120,7 +1290,7 @@ impl Prepared {
let handle_rect = calculate_handle_rect(d, &state.offset);
let interact_id = id.with(d);
let sense = if self.scrolling_enabled {
let sense = if scroll_source.scroll_bar && ui.is_enabled() {
Sense::click_and_drag()
} else {
Sense::hover()
@@ -1170,7 +1340,7 @@ impl Prepared {
// Avoid frame-delay by calculating a new handle rect:
let handle_rect = calculate_handle_rect(d, &state.offset);
let visuals = if scrolling_enabled {
let visuals = if scroll_source.scroll_bar && ui.is_enabled() {
// Pick visuals based on interaction with the handle.
// Remember that the response is for the whole scroll bar!
let is_hovering_handle = response.hovered()

View File

@@ -7,26 +7,49 @@ use emath::Vec2;
pub struct Tooltip<'a> {
pub popup: Popup<'a>,
layer_id: LayerId,
widget_id: Id,
/// The layer of the parent widget.
parent_layer: LayerId,
/// The id of the widget that owns this tooltip.
parent_widget: Id,
}
impl Tooltip<'_> {
/// Show a tooltip that is always open
/// Show a tooltip that is always open.
#[deprecated = "Use `Tooltip::always_open` instead."]
pub fn new(
widget_id: Id,
parent_widget: Id,
ctx: Context,
anchor: impl Into<PopupAnchor>,
layer_id: LayerId,
parent_layer: LayerId,
) -> Self {
Self {
// TODO(lucasmerlin): Set width somehow (we're missing context here)
popup: Popup::new(widget_id, ctx, anchor.into(), layer_id)
popup: Popup::new(parent_widget, ctx, anchor.into(), parent_layer)
.kind(PopupKind::Tooltip)
.gap(4.0)
.sense(Sense::hover()),
layer_id,
widget_id,
parent_layer,
parent_widget,
}
}
/// Show a tooltip that is always open.
pub fn always_open(
ctx: Context,
parent_layer: LayerId,
parent_widget: Id,
anchor: impl Into<PopupAnchor>,
) -> Self {
let width = ctx.style().spacing.tooltip_width;
Self {
popup: Popup::new(parent_widget, ctx, anchor.into(), parent_layer)
.kind(PopupKind::Tooltip)
.gap(4.0)
.width(width)
.sense(Sense::hover()),
parent_layer,
parent_widget,
}
}
@@ -39,8 +62,8 @@ impl Tooltip<'_> {
.sense(Sense::hover());
Self {
popup,
layer_id: response.layer_id,
widget_id: response.id,
parent_layer: response.layer_id,
parent_widget: response.id,
}
}
@@ -96,8 +119,8 @@ impl Tooltip<'_> {
pub fn show<R>(self, content: impl FnOnce(&mut crate::Ui) -> R) -> Option<InnerResponse<R>> {
let Self {
mut popup,
layer_id: parent_layer,
widget_id,
parent_layer,
parent_widget,
} = self;
if !popup.is_open() {
@@ -111,11 +134,11 @@ impl Tooltip<'_> {
fs.layers
.entry(parent_layer)
.or_default()
.widget_with_tooltip = Some(widget_id);
.widget_with_tooltip = Some(parent_widget);
fs.tooltips
.widget_tooltips
.get(&widget_id)
.get(&parent_widget)
.copied()
.unwrap_or(PerWidgetTooltipState {
bounding_rect: rect,
@@ -123,7 +146,7 @@ impl Tooltip<'_> {
})
});
let tooltip_area_id = Self::tooltip_id(widget_id, state.tooltip_count);
let tooltip_area_id = Self::tooltip_id(parent_widget, state.tooltip_count);
popup = popup.anchor(state.bounding_rect).id(tooltip_area_id);
let response = popup.show(|ui| {
@@ -144,7 +167,7 @@ impl Tooltip<'_> {
response
.response
.ctx
.pass_state_mut(|fs| fs.tooltips.widget_tooltips.insert(widget_id, state));
.pass_state_mut(|fs| fs.tooltips.widget_tooltips.insert(parent_widget, state));
Self::remember_that_tooltip_was_shown(&response.response.ctx);
}

View File

@@ -8,7 +8,7 @@ use epaint::{CornerRadiusF32, RectShape};
use crate::collapsing_header::CollapsingState;
use crate::*;
use super::scroll_area::ScrollBarVisibility;
use super::scroll_area::{ScrollBarVisibility, ScrollSource};
use super::{area, resize, Area, Frame, Resize, ScrollArea};
/// Builder for a floating window which can be dragged, closed, collapsed, resized and scrolled (off by default).
@@ -376,14 +376,6 @@ impl<'open> Window<'open> {
self
}
/// Enable/disable horizontal/vertical scrolling. `false` by default.
#[deprecated = "Renamed to `scroll`"]
#[inline]
pub fn scroll2(mut self, scroll: impl Into<Vec2b>) -> Self {
self.scroll = self.scroll.scroll(scroll);
self
}
/// Enable/disable horizontal scrolling. `false` by default.
#[inline]
pub fn hscroll(mut self, hscroll: bool) -> Self {
@@ -403,7 +395,10 @@ impl<'open> Window<'open> {
/// See [`ScrollArea::drag_to_scroll`] for more.
#[inline]
pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self {
self.scroll = self.scroll.drag_to_scroll(drag_to_scroll);
self.scroll = self.scroll.scroll_source(ScrollSource {
drag: drag_to_scroll,
..Default::default()
});
self
}

View File

@@ -2,29 +2,26 @@
use std::{borrow::Cow, cell::RefCell, panic::Location, sync::Arc, time::Duration};
use containers::area::AreaState;
use emath::GuiRounding as _;
use emath::{GuiRounding as _, OrderedFloat};
use epaint::{
emath::{self, TSTransform},
mutex::RwLock,
stats::PaintStats,
tessellator,
text::{FontInsert, FontPriority, Fonts},
util::OrderedFloat,
vec2, ClippedPrimitive, ClippedShape, Color32, ImageData, ImageDelta, Pos2, Rect, StrokeKind,
TessellationOptions, TextureAtlas, TextureId, Vec2,
};
use crate::{
animation_manager::AnimationManager,
containers,
containers::{self, area::AreaState},
data::output::PlatformOutput,
epaint, hit_test,
input_state::{InputState, MultiTouchInfo, PointerEvent},
interaction,
layers::GraphicLayers,
load,
load::{Bytes, Loaders, SizedTexture},
load::{self, Bytes, Loaders, SizedTexture},
memory::{Options, Theme},
os::OperatingSystem,
output::FullOutput,
@@ -34,9 +31,10 @@ use crate::{
viewport::ViewportClass,
Align2, CursorIcon, DeferredViewportUiCallback, FontDefinitions, Grid, Id, ImmediateViewport,
ImmediateViewportRendererCallback, Key, KeyboardShortcut, Label, LayerId, Memory,
ModifierNames, NumExt, Order, Painter, RawInput, Response, RichText, ScrollArea, Sense, Style,
TextStyle, TextureHandle, TextureOptions, Ui, ViewportBuilder, ViewportCommand, ViewportId,
ViewportIdMap, ViewportIdPair, ViewportIdSet, ViewportOutput, Widget, WidgetRect, WidgetText,
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")]
@@ -314,7 +312,7 @@ impl std::fmt::Display for RepaintCause {
impl RepaintCause {
/// Capture the file and line number of the call site.
#[allow(clippy::new_without_default)]
#[expect(clippy::new_without_default)]
#[track_caller]
pub fn new() -> Self {
let caller = Location::caller();
@@ -327,7 +325,6 @@ impl RepaintCause {
/// Capture the file and line number of the call site,
/// as well as add a reason.
#[allow(clippy::new_without_default)]
#[track_caller]
pub fn new_reason(reason: impl Into<Cow<'static, str>>) -> Self {
let caller = Location::caller();
@@ -493,8 +490,9 @@ impl ContextImpl {
new_raw_input,
viewport.repaint.requested_immediate_repaint_prev_pass(),
pixels_per_point,
&self.memory.options,
self.memory.options.input_options,
);
let repaint_after = viewport.input.wants_repaint_after();
let screen_rect = viewport.input.screen_rect;
@@ -556,6 +554,10 @@ impl ContextImpl {
}
self.update_fonts_mut();
if let Some(delay) = repaint_after {
self.request_repaint_after(delay, viewport_id, RepaintCause::new());
}
}
/// Load fonts unless already loaded.
@@ -1160,7 +1162,6 @@ impl Context {
///
/// `allow_focus` should usually be true, unless you call this function multiple times with the
/// same widget, then `allow_focus` should only be true once (like in [`Ui::new`] (true) and [`Ui::remember_min_rect`] (false)).
#[allow(clippy::too_many_arguments)]
pub(crate) fn create_widget(&self, w: WidgetRect, allow_focus: bool) -> Response {
let interested_in_focus = w.enabled
&& w.sense.is_focusable()
@@ -1189,7 +1190,7 @@ impl Context {
self.check_for_id_clash(w.id, w.rect, "widget");
}
#[allow(clippy::let_and_return)]
#[allow(clippy::let_and_return, clippy::allow_attributes)]
let res = self.get_response(w);
#[cfg(feature = "accesskit")]
@@ -1233,13 +1234,6 @@ impl Context {
.map(|widget_rect| self.get_response(widget_rect))
}
/// Returns `true` if the widget with the given `Id` contains the pointer.
#[deprecated = "Use Response.contains_pointer or Context::read_response instead"]
pub fn widget_contains_pointer(&self, id: Id) -> bool {
self.read_response(id)
.is_some_and(|response| response.contains_pointer())
}
/// Do all interaction for an existing widget, without (re-)registering it.
pub(crate) fn get_response(&self, widget_rect: WidgetRect) -> Response {
use response::Flags;
@@ -1434,7 +1428,7 @@ impl Context {
/// figured out from the `target_os`.
///
/// For web, this can be figured out from the user-agent,
/// and is done so by [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe).
/// and is done so by [`eframe`](https://github.com/emilk/egui/tree/main/crates/eframe).
pub fn os(&self) -> OperatingSystem {
self.read(|ctx| ctx.os)
}
@@ -1494,9 +1488,37 @@ impl Context {
self.send_cmd(crate::OutputCommand::CopyImage(image));
}
/// Set the mouse cursor position (if the platform supports it).
pub fn set_pointer_position(&self, position: Pos2) {
self.send_cmd(crate::OutputCommand::SetPointerPosition(position));
fn can_show_modifier_symbols(&self) -> bool {
let ModifierNames {
alt,
ctrl,
shift,
mac_cmd,
..
} = ModifierNames::SYMBOLS;
let font_id = TextStyle::Body.resolve(&self.style());
self.fonts(|f| {
let mut lock = f.lock();
let font = lock.fonts.font(&font_id);
font.has_glyphs(alt)
&& font.has_glyphs(ctrl)
&& font.has_glyphs(shift)
&& font.has_glyphs(mac_cmd)
})
}
/// Format the given modifiers in a human-readable way (e.g. `Ctrl+Shift+X`).
pub fn format_modifiers(&self, modifiers: Modifiers) -> String {
let os = self.os();
let is_mac = os.is_mac();
if is_mac && self.can_show_modifier_symbols() {
ModifierNames::SYMBOLS.format(&modifiers, is_mac)
} else {
ModifierNames::NAMES.format(&modifiers, is_mac)
}
}
/// Format the given shortcut in a human-readable way (e.g. `Ctrl+Shift+X`).
@@ -1505,29 +1527,9 @@ impl Context {
pub fn format_shortcut(&self, shortcut: &KeyboardShortcut) -> String {
let os = self.os();
let is_mac = matches!(os, OperatingSystem::Mac | OperatingSystem::IOS);
let is_mac = os.is_mac();
let can_show_symbols = || {
let ModifierNames {
alt,
ctrl,
shift,
mac_cmd,
..
} = ModifierNames::SYMBOLS;
let font_id = TextStyle::Body.resolve(&self.style());
self.fonts(|f| {
let mut lock = f.lock();
let font = lock.fonts.font(&font_id);
font.has_glyphs(alt)
&& font.has_glyphs(ctrl)
&& font.has_glyphs(shift)
&& font.has_glyphs(mac_cmd)
})
};
if is_mac && can_show_symbols() {
if is_mac && self.can_show_modifier_symbols() {
shortcut.format(&ModifierNames::SYMBOLS, is_mac)
} else {
shortcut.format(&ModifierNames::NAMES, is_mac)
@@ -2322,6 +2324,8 @@ impl ContextImpl {
let viewport = self.viewports.entry(ended_viewport_id).or_default();
let pixels_per_point = viewport.input.pixels_per_point;
self.loaders.end_pass(viewport.repaint.cumulative_pass_nr);
viewport.repaint.cumulative_pass_nr += 1;
self.memory.end_pass(&viewport.this_pass.used_ids);
@@ -2355,7 +2359,6 @@ impl ContextImpl {
// Inform the backend of all textures that have been updated (including font atlas).
let textures_delta = self.tex_manager.0.write().take_delta();
#[cfg_attr(not(feature = "accesskit"), allow(unused_mut))]
let mut platform_output: PlatformOutput = std::mem::take(&mut viewport.output);
#[cfg(feature = "accesskit")]
@@ -2400,10 +2403,7 @@ impl ContextImpl {
if repaint_needed {
self.request_repaint(ended_viewport_id, RepaintCause::new());
} else if let Some(delay) = viewport.input.wants_repaint_after() {
self.request_repaint_after(delay, ended_viewport_id, RepaintCause::new());
}
// -------------------
let all_viewport_ids = self.all_viewport_ids();
@@ -2656,7 +2656,7 @@ impl Context {
/// Is an egui context menu open?
///
/// This only works with the old, deprecated [`crate::menu`] API.
#[allow(deprecated)]
#[expect(deprecated)]
#[deprecated = "Use `is_popup_open` instead"]
pub fn is_context_menu_open(&self) -> bool {
self.data(|d| {
@@ -2746,21 +2746,6 @@ impl Context {
.map(|t| t.inverse())
}
/// Move all the graphics at the given layer.
///
/// Is used to implement drag-and-drop preview.
///
/// This only applied to the existing graphics at the layer, not to new graphics added later.
///
/// For a persistent transform, use [`Self::set_transform_layer`] instead.
#[deprecated = "Use `transform_layer_shapes` instead"]
pub fn translate_layer(&self, layer_id: LayerId, delta: Vec2) {
if delta != Vec2::ZERO {
let transform = emath::TSTransform::from_translation(delta);
self.transform_layer_shapes(layer_id, transform);
}
}
/// Transform all the graphics at the given layer.
///
/// Is used to implement drag-and-drop preview.
@@ -3070,6 +3055,12 @@ impl Context {
self.texture_ui(ui);
});
CollapsingHeader::new("🖼 Image loaders")
.default_open(false)
.show(ui, |ui| {
self.loaders_ui(ui);
});
CollapsingHeader::new("🔠 Font texture")
.default_open(false)
.show(ui, |ui| {
@@ -3114,6 +3105,8 @@ impl Context {
));
let max_preview_size = vec2(48.0, 32.0);
let pixels_per_point = self.pixels_per_point();
ui.group(|ui| {
ScrollArea::vertical()
.max_height(300.0)
@@ -3128,15 +3121,16 @@ impl Context {
.show(ui, |ui| {
for (&texture_id, meta) in textures {
let [w, h] = meta.size;
let point_size = vec2(w as f32, h as f32) / pixels_per_point;
let mut size = vec2(w as f32, h as f32);
let mut size = point_size;
size *= (max_preview_size.x / size.x).min(1.0);
size *= (max_preview_size.y / size.y).min(1.0);
ui.image(SizedTexture::new(texture_id, size))
.on_hover_ui(|ui| {
// show larger on hover
let max_size = 0.5 * ui.ctx().screen_rect().size();
let mut size = vec2(w as f32, h as f32);
let mut size = point_size;
size *= max_size.x / size.x.max(max_size.x);
size *= max_size.y / size.y.max(max_size.y);
ui.image(SizedTexture::new(texture_id, size));
@@ -3152,6 +3146,73 @@ impl Context {
});
}
/// Show stats about different image loaders.
pub fn loaders_ui(&self, ui: &mut crate::Ui) {
struct LoaderInfo {
id: String,
byte_size: usize,
}
let mut byte_loaders = vec![];
let mut image_loaders = vec![];
let mut texture_loaders = vec![];
{
let loaders = self.loaders();
let Loaders {
include: _,
bytes,
image,
texture,
} = loaders.as_ref();
for loader in bytes.lock().iter() {
byte_loaders.push(LoaderInfo {
id: loader.id().to_owned(),
byte_size: loader.byte_size(),
});
}
for loader in image.lock().iter() {
image_loaders.push(LoaderInfo {
id: loader.id().to_owned(),
byte_size: loader.byte_size(),
});
}
for loader in texture.lock().iter() {
texture_loaders.push(LoaderInfo {
id: loader.id().to_owned(),
byte_size: loader.byte_size(),
});
}
}
fn loaders_ui(ui: &mut crate::Ui, title: &str, loaders: &[LoaderInfo]) {
let heading = format!("{} {title} loaders", loaders.len());
crate::CollapsingHeader::new(heading)
.default_open(true)
.show(ui, |ui| {
Grid::new("loaders")
.striped(true)
.num_columns(2)
.show(ui, |ui| {
ui.label("ID");
ui.label("Size");
ui.end_row();
for loader in loaders {
ui.label(&loader.id);
ui.label(format!("{:.3} MB", loader.byte_size as f64 * 1e-6));
ui.end_row();
}
});
});
}
loaders_ui(ui, "byte", &byte_loaders);
loaders_ui(ui, "image", &image_loaders);
loaders_ui(ui, "texture", &texture_loaders);
}
/// Shows the contents of [`Self::memory`].
pub fn memory_ui(&self, ui: &mut crate::Ui) {
if ui
@@ -3210,7 +3271,7 @@ impl Context {
}
});
#[allow(deprecated)]
#[expect(deprecated)]
ui.horizontal(|ui| {
ui.label(format!(
"{} menu bars",
@@ -3268,7 +3329,7 @@ impl Context {
/// the function is still called, but with no other effect.
///
/// No locks are held while the given closure is called.
#[allow(clippy::unused_self, clippy::let_and_return)]
#[allow(clippy::unused_self, clippy::let_and_return, clippy::allow_attributes)]
#[inline]
pub fn with_accessibility_parent<R>(&self, _id: Id, f: impl FnOnce() -> R) -> R {
// TODO(emilk): this isn't thread-safe - another thread can call this function between the push/pop calls
@@ -3452,9 +3513,10 @@ impl Context {
// Try most recently added loaders first (hence `.rev()`)
for loader in bytes_loaders.iter().rev() {
match loader.load(self, uri) {
Err(load::LoadError::NotSupported) => continue,
result => return result,
let result = loader.load(self, uri);
match result {
Err(load::LoadError::NotSupported) => {}
_ => return result,
}
}
@@ -3495,10 +3557,9 @@ impl Context {
// Try most recently added loaders first (hence `.rev()`)
for loader in image_loaders.iter().rev() {
match loader.load(self, uri, size_hint) {
Err(load::LoadError::NotSupported) => continue,
Err(load::LoadError::NotSupported) => {}
Err(load::LoadError::FormatNotSupported { detected_format }) => {
format = format.or(detected_format);
continue;
}
result => return result,
}
@@ -3541,7 +3602,7 @@ impl Context {
// Try most recently added loaders first (hence `.rev()`)
for loader in texture_loaders.iter().rev() {
match loader.load(self, uri, texture_options, size_hint) {
Err(load::LoadError::NotSupported) => continue,
Err(load::LoadError::NotSupported) => {}
result => return result,
}
}
@@ -3553,6 +3614,14 @@ impl Context {
pub fn loaders(&self) -> Arc<Loaders> {
self.read(|this| this.loaders.clone())
}
/// Returns `true` if any image is currently being loaded.
pub fn has_pending_images(&self) -> bool {
self.read(|this| {
this.loaders.image.lock().iter().any(|i| i.has_pending())
|| this.loaders.bytes.lock().iter().any(|i| i.has_pending())
})
}
}
/// ## Viewports
@@ -3601,7 +3670,6 @@ impl Context {
/// * Set the window attributes (position, size, …) based on [`ImmediateViewport::builder`].
/// * Call [`Context::run`] with [`ImmediateViewport::viewport_ui_cb`].
/// * Handle the output from [`Context::run`], including rendering
#[allow(clippy::unused_self)]
pub fn set_immediate_viewport_renderer(
callback: impl for<'a> Fn(&Self, ImmediateViewport<'a>) + 'static,
) {

View File

@@ -256,9 +256,7 @@ impl ViewportInfo {
/// If this is not the root viewport,
/// it is up to the user to hide this viewport the next frame.
pub fn close_requested(&self) -> bool {
self.events
.iter()
.any(|&event| event == ViewportEvent::Close)
self.events.contains(&ViewportEvent::Close)
}
/// Helper: move [`Self::events`], clone the other fields.
@@ -592,7 +590,7 @@ pub const NUM_POINTER_BUTTONS: usize = 5;
/// State of the modifier keys. These must be fed to egui.
///
/// The best way to compare [`Modifiers`] is by using [`Modifiers::matches`].
/// The best way to compare [`Modifiers`] is by using [`Modifiers::matches_logically`] or [`Modifiers::matches_exact`].
///
/// NOTE: For cross-platform uses, ALT+SHIFT is a bad combination of modifiers
/// as on mac that is how you type special characters,
@@ -775,8 +773,8 @@ impl Modifiers {
/// ```
/// # use egui::Modifiers;
/// # let pressed_modifiers = Modifiers::default();
/// if pressed_modifiers.matches(Modifiers::ALT | Modifiers::SHIFT) {
/// // Alt and Shift are pressed, and nothing else
/// if pressed_modifiers.matches_logically(Modifiers::ALT | Modifiers::SHIFT) {
/// // Alt and Shift are pressed, but not ctrl/command
/// }
/// ```
///
@@ -817,7 +815,7 @@ impl Modifiers {
/// ```
/// # use egui::Modifiers;
/// # let pressed_modifiers = Modifiers::default();
/// if pressed_modifiers.matches(Modifiers::ALT | Modifiers::SHIFT) {
/// if pressed_modifiers.matches_exact(Modifiers::ALT | Modifiers::SHIFT) {
/// // Alt and Shift are pressed, and nothing else
/// }
/// ```
@@ -825,13 +823,13 @@ impl Modifiers {
/// ## Behavior:
/// ```
/// # use egui::Modifiers;
/// assert!(Modifiers::CTRL.matches(Modifiers::CTRL));
/// assert!(!Modifiers::CTRL.matches(Modifiers::CTRL | Modifiers::SHIFT));
/// assert!(!(Modifiers::CTRL | Modifiers::SHIFT).matches(Modifiers::CTRL));
/// assert!((Modifiers::CTRL | Modifiers::COMMAND).matches(Modifiers::CTRL));
/// assert!((Modifiers::CTRL | Modifiers::COMMAND).matches(Modifiers::COMMAND));
/// assert!((Modifiers::MAC_CMD | Modifiers::COMMAND).matches(Modifiers::COMMAND));
/// assert!(!Modifiers::COMMAND.matches(Modifiers::MAC_CMD));
/// assert!(Modifiers::CTRL.matches_exact(Modifiers::CTRL));
/// assert!(!Modifiers::CTRL.matches_exact(Modifiers::CTRL | Modifiers::SHIFT));
/// assert!(!(Modifiers::CTRL | Modifiers::SHIFT).matches_exact(Modifiers::CTRL));
/// assert!((Modifiers::CTRL | Modifiers::COMMAND).matches_exact(Modifiers::CTRL));
/// assert!((Modifiers::CTRL | Modifiers::COMMAND).matches_exact(Modifiers::COMMAND));
/// assert!((Modifiers::MAC_CMD | Modifiers::COMMAND).matches_exact(Modifiers::COMMAND));
/// assert!(!Modifiers::COMMAND.matches_exact(Modifiers::MAC_CMD));
/// ```
pub fn matches_exact(&self, pattern: Self) -> bool {
// alt and shift must always match the pattern:
@@ -842,9 +840,35 @@ impl Modifiers {
self.cmd_ctrl_matches(pattern)
}
#[deprecated = "Renamed `matches_exact`, but maybe you want to use `matches_logically` instead"]
pub fn matches(&self, pattern: Self) -> bool {
self.matches_exact(pattern)
/// Check if any of the modifiers match exactly.
///
/// Returns true if the same modifier is pressed in `self` as in `pattern`,
/// for at least one modifier.
///
/// ## Behavior:
/// ```
/// # use egui::Modifiers;
/// assert!(Modifiers::CTRL.matches_any(Modifiers::CTRL));
/// assert!(Modifiers::CTRL.matches_any(Modifiers::CTRL | Modifiers::SHIFT));
/// assert!((Modifiers::CTRL | Modifiers::SHIFT).matches_any(Modifiers::CTRL));
/// ```
pub fn matches_any(&self, pattern: Self) -> bool {
if self.alt && pattern.alt {
return true;
}
if self.shift && pattern.shift {
return true;
}
if self.ctrl && pattern.ctrl {
return true;
}
if self.mac_cmd && pattern.mac_cmd {
return true;
}
if (self.mac_cmd || self.command || self.ctrl) && pattern.command {
return true;
}
false
}
/// Checks only cmd/ctrl, not alt/shift.
@@ -960,6 +984,12 @@ impl std::ops::BitOrAssign for Modifiers {
}
}
impl Modifiers {
pub fn ui(&self, ui: &mut crate::Ui) {
ui.label(ModifierNames::NAMES.format(self, ui.ctx().os().is_mac()));
}
}
// ----------------------------------------------------------------------------
/// Names of different modifier keys.
@@ -1233,7 +1263,7 @@ pub struct EventFilter {
pub escape: bool,
}
#[allow(clippy::derivable_impls)] // let's be explicit
#[expect(clippy::derivable_impls)] // let's be explicit
impl Default for EventFilter {
fn default() -> Self {
Self {

View File

@@ -183,6 +183,11 @@ pub enum Key {
F33,
F34,
F35,
/// Back navigation key from multimedia keyboard.
/// Android sends this key on Back button press.
/// Does not work on Web.
BrowserBack,
// When adding keys, remember to also update:
// * crates/egui-winit/src/lib.rs
// * Key::ALL
@@ -307,6 +312,8 @@ impl Key {
Self::F33,
Self::F34,
Self::F35,
// Navigation keys:
Self::BrowserBack,
];
/// Converts `"A"` to `Key::A`, `Space` to `Key::Space`, etc.
@@ -435,6 +442,8 @@ impl Key {
"F34" => Self::F34,
"F35" => Self::F35,
"BrowserBack" => Self::BrowserBack,
_ => return None,
})
}
@@ -588,6 +597,8 @@ impl Key {
Self::F33 => "F33",
Self::F34 => "F34",
Self::F35 => "F35",
Self::BrowserBack => "BrowserBack",
}
}
}
@@ -596,7 +607,7 @@ impl Key {
fn test_key_from_name() {
assert_eq!(
Key::ALL.len(),
Key::F35 as usize + 1,
Key::BrowserBack as usize + 1,
"Some keys are missing in Key::ALL"
);

View File

@@ -95,9 +95,6 @@ pub enum OutputCommand {
/// Open this url in a browser.
OpenUrl(OpenUrl),
/// Set the mouse cursor position (if the platform supports it).
SetPointerPosition(emath::Pos2),
}
/// The non-rendering part of what egui emits each frame.
@@ -255,7 +252,7 @@ pub struct OpenUrl {
}
impl OpenUrl {
#[allow(clippy::needless_pass_by_value)]
#[expect(clippy::needless_pass_by_value)]
pub fn same_tab(url: impl ToString) -> Self {
Self {
url: url.to_string(),
@@ -263,7 +260,7 @@ impl OpenUrl {
}
}
#[allow(clippy::needless_pass_by_value)]
#[expect(clippy::needless_pass_by_value)]
pub fn new_tab(url: impl ToString) -> Self {
Self {
url: url.to_string(),
@@ -610,7 +607,7 @@ impl WidgetInfo {
}
}
#[allow(clippy::needless_pass_by_value)]
#[expect(clippy::needless_pass_by_value)]
pub fn labeled(typ: WidgetType, enabled: bool, label: impl ToString) -> Self {
Self {
enabled,
@@ -620,7 +617,7 @@ impl WidgetInfo {
}
/// checkboxes, radio-buttons etc
#[allow(clippy::needless_pass_by_value)]
#[expect(clippy::needless_pass_by_value)]
pub fn selected(typ: WidgetType, enabled: bool, selected: bool, label: impl ToString) -> Self {
Self {
enabled,
@@ -638,7 +635,7 @@ impl WidgetInfo {
}
}
#[allow(clippy::needless_pass_by_value)]
#[expect(clippy::needless_pass_by_value)]
pub fn slider(enabled: bool, value: f64, label: impl ToString) -> Self {
let label = label.to_string();
Self {
@@ -649,7 +646,7 @@ impl WidgetInfo {
}
}
#[allow(clippy::needless_pass_by_value)]
#[expect(clippy::needless_pass_by_value)]
pub fn text_edit(
enabled: bool,
prev_text_value: impl ToString,
@@ -673,7 +670,7 @@ impl WidgetInfo {
}
}
#[allow(clippy::needless_pass_by_value)]
#[expect(clippy::needless_pass_by_value)]
pub fn text_selection_changed(
enabled: bool,
text_selection: std::ops::RangeInclusive<usize>,
@@ -742,7 +739,7 @@ impl WidgetInfo {
if text_value.is_empty() {
"blank".into()
} else {
text_value.to_string()
text_value.clone()
}
} else {
"blank".into()

View File

@@ -13,7 +13,7 @@ use crate::{Context, CursorIcon, Id};
/// - [`crate::Response::dnd_hover_payload`]
/// - [`crate::Response::dnd_release_payload`]
///
/// See [this example](https://github.com/emilk/egui/blob/master/crates/egui_demo_lib/src/demo/drag_and_drop.rs).
/// See [this example](https://github.com/emilk/egui/blob/main/crates/egui_demo_lib/src/demo/drag_and_drop.rs).
#[doc(alias = "drag and drop")]
#[derive(Clone, Default)]
pub struct DragAndDrop {

View File

@@ -1,8 +1,8 @@
use emath::GuiRounding as _;
use crate::{
vec2, Align2, Color32, Context, Id, InnerResponse, NumExt, Painter, Rect, Region, Style, Ui,
UiBuilder, Vec2,
vec2, Align2, Color32, Context, Id, InnerResponse, NumExt as _, Painter, Rect, Region, Style,
Ui, UiBuilder, Vec2,
};
#[cfg(debug_assertions)]
@@ -184,7 +184,7 @@ impl GridLayout {
Rect::from_min_size(cursor.min, size).round_ui()
}
#[allow(clippy::unused_self)]
#[expect(clippy::unused_self)]
pub(crate) fn align_size_within_rect(&self, size: Vec2, frame: Rect) -> Rect {
// TODO(emilk): allow this alignment to be customized
Align2::LEFT_CENTER

View File

@@ -65,7 +65,7 @@ pub fn hit_test(
.filter(|layer| layer.order.allow_interaction())
.flat_map(|&layer_id| widgets.get_layer(layer_id))
.filter(|&w| {
if w.interact_rect.is_negative() {
if w.interact_rect.is_negative() || w.interact_rect.any_nan() {
return false;
}

View File

@@ -59,7 +59,7 @@ impl Id {
/// Generate a new [`Id`] by hashing the parent [`Id`] and the given argument.
pub fn with(self, child: impl std::hash::Hash) -> Self {
use std::hash::{BuildHasher, Hasher};
use std::hash::{BuildHasher as _, Hasher as _};
let mut hasher = ahash::RandomState::with_seeds(1, 2, 3, 4).build_hasher();
hasher.write_u64(self.0.get());
child.hash(&mut hasher);

View File

@@ -5,7 +5,7 @@ use crate::data::input::{
TouchDeviceId, ViewportInfo, NUM_POINTER_BUTTONS,
};
use crate::{
emath::{vec2, NumExt, Pos2, Rect, Vec2},
emath::{vec2, NumExt as _, Pos2, Rect, Vec2},
util::History,
};
use std::{
@@ -18,9 +18,15 @@ pub use touch_state::MultiTouchInfo;
use touch_state::TouchState;
/// Options for input state handling.
#[derive(Clone, Debug, PartialEq)]
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct InputOptions {
/// Multiplier for the scroll speed when reported in [`crate::MouseWheelUnit::Line`]s.
pub line_scroll_speed: f32,
/// Controls the speed at which we zoom in when doing ctrl/cmd + scroll.
pub scroll_zoom_speed: f32,
/// After a pointer-down event, if the pointer moves more than this, it won't become a click.
pub max_click_dist: f32,
@@ -35,14 +41,44 @@ pub struct InputOptions {
/// The new pointer press must come within this many seconds from previous pointer release
/// for double click (or when this value is doubled, triple click) to count.
pub max_double_click_delay: f64,
/// When this modifier is down, all scroll events are treated as zoom events.
///
/// The default is CTRL/CMD, and it is STRONGLY recommended to NOT change this.
pub zoom_modifier: Modifiers,
/// When this modifier is down, all scroll events are treated as horizontal scrolls,
/// and when combined with [`Self::zoom_modifier`] it will result in zooming
/// on only the horizontal axis.
///
/// The default is SHIFT, and it is STRONGLY recommended to NOT change this.
pub horizontal_scroll_modifier: Modifiers,
/// When this modifier is down, all scroll events are treated as vertical scrolls,
/// and when combined with [`Self::zoom_modifier`] it will result in zooming
/// on only the vertical axis.
pub vertical_scroll_modifier: Modifiers,
}
impl Default for InputOptions {
fn default() -> Self {
// TODO(emilk): figure out why these constants need to be different on web and on native (winit).
let is_web = cfg!(target_arch = "wasm32");
let line_scroll_speed = if is_web {
8.0
} else {
40.0 // Scroll speed decided by consensus: https://github.com/emilk/egui/issues/461
};
Self {
line_scroll_speed,
scroll_zoom_speed: 1.0 / 200.0,
max_click_dist: 6.0,
max_click_duration: 0.8,
max_double_click_delay: 0.3,
zoom_modifier: Modifiers::COMMAND,
horizontal_scroll_modifier: Modifiers::SHIFT,
vertical_scroll_modifier: Modifiers::ALT,
}
}
}
@@ -51,39 +87,74 @@ impl InputOptions {
/// Show the options in the ui.
pub fn ui(&mut self, ui: &mut crate::Ui) {
let Self {
line_scroll_speed,
scroll_zoom_speed,
max_click_dist,
max_click_duration,
max_double_click_delay,
zoom_modifier,
horizontal_scroll_modifier,
vertical_scroll_modifier,
} = self;
crate::containers::CollapsingHeader::new("InputOptions")
.default_open(false)
crate::Grid::new("InputOptions")
.num_columns(2)
.striped(true)
.show(ui, |ui| {
ui.horizontal(|ui| {
ui.label("Max click distance");
ui.add(
crate::DragValue::new(max_click_dist)
.range(0.0..=f32::INFINITY)
ui.label("Line scroll speed");
ui.add(crate::DragValue::new(line_scroll_speed).range(0.0..=f32::INFINITY))
.on_hover_text(
"How many lines to scroll with each tick of the mouse wheel",
);
ui.end_row();
ui.label("Scroll zoom speed");
ui.add(
crate::DragValue::new(scroll_zoom_speed)
.range(0.0..=f32::INFINITY)
.speed(0.001),
)
.on_hover_text("How fast to zoom with ctrl/cmd + scroll");
ui.end_row();
ui.label("Max click distance");
ui.add(crate::DragValue::new(max_click_dist).range(0.0..=f32::INFINITY))
.on_hover_text(
"If the pointer moves more than this, it won't become a click",
);
ui.end_row();
ui.label("Max click duration");
ui.add(
crate::DragValue::new(max_click_duration)
.range(0.1..=f64::INFINITY)
.speed(0.1),
)
.on_hover_text("If the pointer moves more than this, it won't become a click");
});
ui.horizontal(|ui| {
ui.label("Max click duration");
ui.add(
crate::DragValue::new(max_click_duration)
.range(0.1..=f64::INFINITY)
.speed(0.1),
)
.on_hover_text("If the pointer is down for longer than this it will no longer register as a click");
});
ui.horizontal(|ui| {
ui.label("Max double click delay");
ui.add(
crate::DragValue::new(max_double_click_delay)
.range(0.01..=f64::INFINITY)
.speed(0.1),
)
.on_hover_text("Max time interval for double click to count");
});
.on_hover_text(
"If the pointer is down for longer than this it will no longer register as a click",
);
ui.end_row();
ui.label("Max double click delay");
ui.add(
crate::DragValue::new(max_double_click_delay)
.range(0.01..=f64::INFINITY)
.speed(0.1),
)
.on_hover_text("Max time interval for double click to count");
ui.end_row();
ui.label("zoom_modifier");
zoom_modifier.ui(ui);
ui.end_row();
ui.label("horizontal_scroll_modifier");
horizontal_scroll_modifier.ui(ui);
ui.end_row();
ui.label("vertical_scroll_modifier");
vertical_scroll_modifier.ui(ui);
ui.end_row();
});
}
}
@@ -227,7 +298,7 @@ pub struct InputState {
/// Input state management configuration.
///
/// This gets copied from `egui::Options` at the start of each frame for convenience.
input_options: InputOptions,
options: InputOptions,
}
impl Default for InputState {
@@ -255,7 +326,7 @@ impl Default for InputState {
modifiers: Default::default(),
keys_down: Default::default(),
events: Default::default(),
input_options: Default::default(),
options: Default::default(),
}
}
}
@@ -267,7 +338,7 @@ impl InputState {
mut new: RawInput,
requested_immediate_repaint_prev_frame: bool,
pixels_per_point: f32,
options: &crate::Options,
options: InputOptions,
) -> Self {
profiling::function_scope!();
@@ -324,11 +395,18 @@ impl InputState {
MouseWheelUnit::Page => screen_rect.height() * *delta,
};
if modifiers.shift {
// Treat as horizontal scrolling.
let is_horizontal = modifiers.matches_any(options.horizontal_scroll_modifier);
let is_vertical = modifiers.matches_any(options.vertical_scroll_modifier);
if is_horizontal && !is_vertical {
// Treat all scrolling as horizontal scrolling.
// Note: one Mac we already get horizontal scroll events when shift is down.
delta = vec2(delta.x + delta.y, 0.0);
}
if !is_horizontal && is_vertical {
// Treat all scrolling as vertical scrolling.
delta = vec2(0.0, delta.x + delta.y);
}
raw_scroll_delta += delta;
@@ -342,14 +420,14 @@ impl InputState {
MouseWheelUnit::Line | MouseWheelUnit::Page => false,
};
let is_zoom = modifiers.ctrl || modifiers.mac_cmd || modifiers.command;
let is_zoom = modifiers.matches_any(options.zoom_modifier);
#[allow(clippy::collapsible_else_if)]
#[expect(clippy::collapsible_else_if)]
if is_zoom {
if is_smooth {
smooth_scroll_delta_for_zoom += delta.y;
smooth_scroll_delta_for_zoom += delta.x + delta.y;
} else {
unprocessed_scroll_delta_for_zoom += delta.y;
unprocessed_scroll_delta_for_zoom += delta.x + delta.y;
}
} else {
if is_smooth {
@@ -437,7 +515,7 @@ impl InputState {
keys_down,
events: new.events.clone(), // TODO(emilk): remove clone() and use raw.events
raw: new,
input_options: options.input_options.clone(),
options,
}
}
@@ -452,10 +530,13 @@ impl InputState {
self.screen_rect
}
/// Zoom scale factor this frame (e.g. from ctrl-scroll or pinch gesture).
/// Uniform zoom scale factor this frame (e.g. from ctrl-scroll or pinch gesture).
/// * `zoom = 1`: no change
/// * `zoom < 1`: pinch together
/// * `zoom > 1`: pinch spread
///
/// If your application supports non-proportional zooming,
/// then you probably want to use [`Self::zoom_delta_2d`] instead.
#[inline(always)]
pub fn zoom_delta(&self) -> f32 {
// If a multi touch gesture is detected, it measures the exact and linear proportions of
@@ -485,10 +566,29 @@ impl InputState {
// the distances of the finger tips. It is therefore potentially more accurate than
// `zoom_factor_delta` which is based on the `ctrl-scroll` event which, in turn, may be
// synthesized from an original touch gesture.
self.multi_touch().map_or_else(
|| Vec2::splat(self.zoom_factor_delta),
|touch| touch.zoom_delta_2d,
)
if let Some(multi_touch) = self.multi_touch() {
multi_touch.zoom_delta_2d
} else {
let mut zoom = Vec2::splat(self.zoom_factor_delta);
let is_horizontal = self
.modifiers
.matches_any(self.options.horizontal_scroll_modifier);
let is_vertical = self
.modifiers
.matches_any(self.options.vertical_scroll_modifier);
if is_horizontal && !is_vertical {
// Horizontal-only zooming.
zoom.y = 1.0;
}
if !is_horizontal && is_vertical {
// Vertical-only zooming.
zoom.x = 1.0;
}
zoom
}
}
/// How long has it been (in seconds) since the use last scrolled?
@@ -497,10 +597,14 @@ impl InputState {
(self.time - self.last_scroll_time) as f32
}
/// The [`crate::Context`] will call this at the end of each frame to see if we need a repaint.
/// The [`crate::Context`] will call this at the beginning of each frame to see if we need a repaint.
///
/// Returns how long to wait for a repaint.
pub fn wants_repaint_after(&self) -> Option<Duration> {
///
/// NOTE: It's important to call this immediately after [`Self::begin_pass`] since calls to
/// [`Self::consume_key`] will remove events from the vec, meaning those key presses wouldn't
/// cause a repaint.
pub(crate) fn wants_repaint_after(&self) -> Option<Duration> {
if self.pointer.wants_repaint()
|| self.unprocessed_scroll_delta.abs().max_elem() > 0.2
|| self.unprocessed_scroll_delta_for_zoom.abs() > 0.2
@@ -514,10 +618,10 @@ impl InputState {
// We need to wake up and check for press-and-hold for the context menu.
if let Some(press_start_time) = self.pointer.press_start_time {
let press_duration = self.time - press_start_time;
if self.input_options.max_click_duration.is_finite()
&& press_duration < self.input_options.max_click_duration
if self.options.max_click_duration.is_finite()
&& press_duration < self.options.max_click_duration
{
let secs_until_menu = self.input_options.max_click_duration - press_duration;
let secs_until_menu = self.options.max_click_duration - press_duration;
return Some(Duration::from_secs_f64(secs_until_menu));
}
}
@@ -878,7 +982,7 @@ pub struct PointerState {
/// Input state management configuration.
///
/// This gets copied from `egui::Options` at the start of each frame for convenience.
input_options: InputOptions,
options: InputOptions,
}
impl Default for PointerState {
@@ -901,23 +1005,18 @@ impl Default for PointerState {
last_last_click_time: f64::NEG_INFINITY,
last_move_time: f64::NEG_INFINITY,
pointer_events: vec![],
input_options: Default::default(),
options: Default::default(),
}
}
}
impl PointerState {
#[must_use]
pub(crate) fn begin_pass(
mut self,
time: f64,
new: &RawInput,
options: &crate::Options,
) -> Self {
pub(crate) fn begin_pass(mut self, time: f64, new: &RawInput, options: InputOptions) -> Self {
let was_decidedly_dragging = self.is_decidedly_dragging();
self.time = time;
self.input_options = options.input_options.clone();
self.options = options;
self.pointer_events.clear();
@@ -938,9 +1037,10 @@ impl PointerState {
if let Some(press_origin) = self.press_origin {
self.has_moved_too_much_for_a_click |=
press_origin.distance(pos) > self.input_options.max_click_dist;
press_origin.distance(pos) > self.options.max_click_dist;
}
self.last_move_time = time;
self.pointer_events.push(PointerEvent::Moved(pos));
}
Event::PointerButton {
@@ -976,10 +1076,10 @@ impl PointerState {
let clicked = self.could_any_button_be_click();
let click = if clicked {
let double_click = (time - self.last_click_time)
< self.input_options.max_double_click_delay;
let double_click =
(time - self.last_click_time) < self.options.max_double_click_delay;
let triple_click = (time - self.last_last_click_time)
< (self.input_options.max_double_click_delay * 2.0);
< (self.options.max_double_click_delay * 2.0);
let count = if triple_click {
3
} else if double_click {
@@ -1283,7 +1383,7 @@ impl PointerState {
}
if let Some(press_start_time) = self.press_start_time {
if self.time - press_start_time > self.input_options.max_click_duration {
if self.time - press_start_time > self.options.max_click_duration {
return false;
}
}
@@ -1319,7 +1419,7 @@ impl PointerState {
&& !self.has_moved_too_much_for_a_click
&& self.button_down(PointerButton::Primary)
&& self.press_start_time.is_some_and(|press_start_time| {
self.time - press_start_time > self.input_options.max_click_duration
self.time - press_start_time > self.options.max_click_duration
})
}
@@ -1379,7 +1479,7 @@ impl InputState {
modifiers,
keys_down,
events,
input_options: _,
options: _,
} = self;
ui.style_mut()
@@ -1465,7 +1565,7 @@ impl PointerState {
last_last_click_time,
pointer_events,
last_move_time,
input_options: _,
options: _,
} = self;
ui.label(format!("latest_pos: {latest_pos:?}"));

View File

@@ -194,7 +194,7 @@ impl TouchState {
let zoom_delta = state.current.avg_distance / state_previous.avg_distance;
let zoom_delta2 = match state.pinch_type {
let zoom_delta_2d = match state.pinch_type {
PinchType::Horizontal => Vec2::new(
state.current.avg_abs_distance2.x / state_previous.avg_abs_distance2.x,
1.0,
@@ -213,7 +213,7 @@ impl TouchState {
start_pos: state.start_pointer_pos,
num_touches: self.active_touches.len(),
zoom_delta,
zoom_delta_2d: zoom_delta2,
zoom_delta_2d,
rotation_delta: normalized_angle(state.current.heading - state_previous.heading),
translation_delta: state.current.avg_pos - state_previous.avg_pos,
force: state.current.avg_force,

View File

@@ -1,7 +1,7 @@
//! Showing UI:s for egui/epaint types.
use crate::{
epaint, memory, pos2, remap_clamp, vec2, Color32, CursorIcon, FontFamily, FontId, Label, Mesh,
NumExt, Rect, Response, Sense, Shape, Slider, TextStyle, TextWrapMode, Ui, Widget,
NumExt as _, Rect, Response, Sense, Shape, Slider, TextStyle, TextWrapMode, Ui, Widget,
};
pub fn font_family_ui(ui: &mut Ui, font_family: &mut FontFamily) {

View File

@@ -1,7 +1,7 @@
use emath::GuiRounding as _;
use crate::{
emath::{pos2, vec2, Align2, NumExt, Pos2, Rect, Vec2},
emath::{pos2, vec2, Align2, NumExt as _, Pos2, Rect, Vec2},
Align,
};
const INFINITY: f32 = f32::INFINITY;

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.81.0 or later to use `egui`.
//! You need to have rust 1.84.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).
@@ -143,7 +143,7 @@
//! }
//! ```
//!
//! For a reference OpenGL renderer, see [the `egui_glow` painter](https://github.com/emilk/egui/blob/master/crates/egui_glow/src/painter.rs).
//! For a reference OpenGL renderer, see [the `egui_glow` painter](https://github.com/emilk/egui/blob/main/crates/egui_glow/src/painter.rs).
//!
//!
//! ### Debugging your renderer
@@ -219,7 +219,7 @@
//! This means it is responsibility of the egui user to store the state (`value`) so that it persists between frames.
//!
//! It can be useful to read the code for the toggle switch example widget to get a better understanding
//! of how egui works: <https://github.com/emilk/egui/blob/master/crates/egui_demo_lib/src/demo/toggle_switch.rs>.
//! of how egui works: <https://github.com/emilk/egui/blob/main/crates/egui_demo_lib/src/demo/toggle_switch.rs>.
//!
//! Read more about the pros and cons of immediate mode at <https://github.com/emilk/egui#why-immediate-mode>.
//!
@@ -400,6 +400,9 @@
//! profile-with-puffin = ["profiling/profile-with-puffin"]
//! ```
//!
//! ## Custom allocator
//! egui apps can run significantly (~20%) faster by using a custom allocator, like [mimalloc](https://crates.io/crates/mimalloc) or [talc](https://crates.io/crates/talc).
//!
#![allow(clippy::float_cmp)]
#![allow(clippy::manual_range_contains)]
@@ -441,6 +444,7 @@ mod widget_rect;
pub mod widget_text;
pub mod widgets;
mod atomics;
#[cfg(feature = "callstack")]
#[cfg(debug_assertions)]
mod callstack;
@@ -479,6 +483,7 @@ pub mod text {
}
pub use self::{
atomics::*,
containers::*,
context::{Context, RepaintCause, RequestRepaintInfo},
data::{
@@ -493,7 +498,7 @@ pub use self::{
epaint::text::TextWrapMode,
grid::Grid,
id::{Id, IdMap},
input_state::{InputState, MultiTouchInfo, PointerState},
input_state::{InputOptions, InputState, MultiTouchInfo, PointerState},
layers::{LayerId, Order},
layout::*,
load::SizeHint,
@@ -564,7 +569,7 @@ macro_rules! include_image {
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// ui.add(egui::github_link_file_line!("https://github.com/YOUR/PROJECT/blob/master/", "(source code)"));
/// ui.add(egui::github_link_file_line!("https://github.com/YOUR/PROJECT/blob/main/", "(source code)"));
/// # });
/// ```
#[macro_export]
@@ -579,7 +584,7 @@ macro_rules! github_link_file_line {
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// ui.add(egui::github_link_file!("https://github.com/YOUR/PROJECT/blob/master/", "(source code)"));
/// ui.add(egui::github_link_file!("https://github.com/YOUR/PROJECT/blob/main/", "(source code)"));
/// # });
/// ```
#[macro_export]

View File

@@ -64,7 +64,7 @@ use std::{
use ahash::HashMap;
use emath::{Float, OrderedFloat};
use emath::{Float as _, OrderedFloat};
use epaint::{mutex::Mutex, textures::TextureOptions, ColorImage, TextureHandle, TextureId, Vec2};
use crate::Context;
@@ -143,22 +143,53 @@ pub type Result<T, E = LoadError> = std::result::Result<T, E>;
/// Given as a hint for image loading requests.
///
/// Used mostly for rendering SVG:s to a good size.
/// The size is measured in texels, with the pixels per point already factored in.
///
/// All variants will preserve the original aspect ratio.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
/// The [`SizeHint`] determines at what resolution the image should be rasterized.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum SizeHint {
/// Scale original size by some factor.
/// Scale original size by some factor, keeping the original aspect ratio.
///
/// The original size of the image is usually its texel resolution,
/// but for an SVG it's the point size of the SVG.
///
/// For instance, setting `Scale(2.0)` will rasterize SVG:s to twice their original size,
/// which is useful for high-DPI displays.
Scale(OrderedFloat<f32>),
/// Scale to width.
/// Scale to exactly this pixel width, keeping the original aspect ratio.
Width(u32),
/// Scale to height.
/// Scale to exactly this pixel height, keeping the original aspect ratio.
Height(u32),
/// Scale to size.
Size(u32, u32),
/// Scale to this pixel size.
Size {
width: u32,
height: u32,
/// If true, the image will be as large as possible
/// while still fitting within the given width/height.
maintain_aspect_ratio: bool,
},
}
impl SizeHint {
/// Multiply size hint by a factor.
pub fn scale_by(self, factor: f32) -> Self {
match self {
Self::Scale(scale) => Self::Scale(OrderedFloat(factor * scale.0)),
Self::Width(width) => Self::Width((factor * width as f32).round() as _),
Self::Height(height) => Self::Height((factor * height as f32).round() as _),
Self::Size {
width,
height,
maintain_aspect_ratio,
} => Self::Size {
width: (factor * width as f32).round() as _,
height: (factor * height as f32).round() as _,
maintain_aspect_ratio,
},
}
}
}
impl Default for SizeHint {
@@ -168,13 +199,6 @@ impl Default for SizeHint {
}
}
impl From<Vec2> for SizeHint {
#[inline]
fn from(value: Vec2) -> Self {
Self::Size(value.x.round() as u32, value.y.round() as u32)
}
}
/// Represents a byte buffer.
///
/// This is essentially `Cow<'static, [u8]>` but with the `Owned` variant being an `Arc`.
@@ -249,12 +273,16 @@ impl Deref for Bytes {
pub enum BytesPoll {
/// Bytes are being loaded.
Pending {
/// Point size of the image.
///
/// Set if known (e.g. from a HTTP header, or by parsing the image file header).
size: Option<Vec2>,
},
/// Bytes are loaded.
Ready {
/// Point size of the image.
///
/// Set if known (e.g. from a HTTP header, or by parsing the image file header).
size: Option<Vec2>,
@@ -322,12 +350,17 @@ pub trait BytesLoader {
/// Implementations may use this to perform work at the end of a frame,
/// such as evicting unused entries from a cache.
fn end_pass(&self, frame_index: usize) {
let _ = frame_index;
fn end_pass(&self, pass_index: u64) {
let _ = pass_index;
}
/// If the loader caches any data, this should return the size of that cache.
fn byte_size(&self) -> usize;
/// Returns `true` if some data is currently being loaded.
fn has_pending(&self) -> bool {
false
}
}
/// Represents an image which is currently being loaded.
@@ -339,6 +372,8 @@ pub trait BytesLoader {
pub enum ImagePoll {
/// Image is loading.
Pending {
/// Point size of the image.
///
/// Set if known (e.g. from a HTTP header, or by parsing the image file header).
size: Option<Vec2>,
},
@@ -389,18 +424,27 @@ pub trait ImageLoader {
/// Implementations may use this to perform work at the end of a pass,
/// such as evicting unused entries from a cache.
fn end_pass(&self, frame_index: usize) {
let _ = frame_index;
fn end_pass(&self, pass_index: u64) {
let _ = pass_index;
}
/// If the loader caches any data, this should return the size of that cache.
fn byte_size(&self) -> usize;
/// Returns `true` if some image is currently being loaded.
///
/// NOTE: You probably also want to check [`BytesLoader::has_pending`].
fn has_pending(&self) -> bool {
false
}
}
/// A texture with a known size.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SizedTexture {
pub id: TextureId,
/// Point size of the original SVG, or the size of the image in texels.
pub size: Vec2,
}
@@ -446,6 +490,8 @@ impl<'a> From<&'a TextureHandle> for SizedTexture {
pub enum TexturePoll {
/// Texture is loading.
Pending {
/// Point size of the image.
///
/// Set if known (e.g. from a HTTP header, or by parsing the image file header).
size: Option<Vec2>,
},
@@ -455,6 +501,7 @@ pub enum TexturePoll {
}
impl TexturePoll {
/// Point size of the original SVG, or the size of the image in texels.
#[inline]
pub fn size(&self) -> Option<Vec2> {
match self {
@@ -527,8 +574,8 @@ pub trait TextureLoader {
/// Implementations may use this to perform work at the end of a pass,
/// such as evicting unused entries from a cache.
fn end_pass(&self, frame_index: usize) {
let _ = frame_index;
fn end_pass(&self, pass_index: u64) {
let _ = pass_index;
}
/// If the loader caches any data, this should return the size of that cache.
@@ -560,3 +607,26 @@ impl Default for Loaders {
}
}
}
impl Loaders {
/// The given pass has just ended.
pub fn end_pass(&self, pass_index: u64) {
let Self {
include,
bytes,
image,
texture,
} = self;
include.end_pass(pass_index);
for loader in bytes.lock().iter() {
loader.end_pass(pass_index);
}
for loader in image.lock().iter() {
loader.end_pass(pass_index);
}
for loader in texture.lock().iter() {
loader.end_pass(pass_index);
}
}
}

View File

@@ -28,7 +28,7 @@ impl DefaultBytesLoader {
}
impl BytesLoader for DefaultBytesLoader {
fn id(&self) -> &str {
fn id(&self) -> &'static str {
generate_loader_id!(DefaultBytesLoader)
}

View File

@@ -1,17 +1,38 @@
use std::borrow::Cow;
use std::sync::atomic::{AtomicU64, Ordering::Relaxed};
use emath::Vec2;
use super::{
BytesLoader, Context, HashMap, ImagePoll, Mutex, SizeHint, SizedTexture, TextureHandle,
BytesLoader as _, Context, HashMap, ImagePoll, Mutex, SizeHint, SizedTexture, TextureHandle,
TextureLoadResult, TextureLoader, TextureOptions, TexturePoll,
};
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
struct PrimaryKey {
uri: String,
texture_options: TextureOptions,
}
/// SVG:s might have several different sizes loaded
type Bucket = HashMap<Option<SizeHint>, Entry>;
struct Entry {
last_used: AtomicU64,
/// Size of the original SVG, if any, or the texel size of the image if not an SVG.
source_size: Vec2,
handle: TextureHandle,
}
#[derive(Default)]
pub struct DefaultTextureLoader {
cache: Mutex<HashMap<(Cow<'static, str>, TextureOptions), TextureHandle>>,
pass_index: AtomicU64,
cache: Mutex<HashMap<PrimaryKey, Bucket>>,
}
impl TextureLoader for DefaultTextureLoader {
fn id(&self) -> &str {
fn id(&self) -> &'static str {
crate::generate_loader_id!(DefaultTextureLoader)
}
@@ -22,17 +43,47 @@ impl TextureLoader for DefaultTextureLoader {
texture_options: TextureOptions,
size_hint: SizeHint,
) -> TextureLoadResult {
let svg_size_hint = if is_svg(uri) {
// For SVGs it's important that we render at the desired size,
// or we might get a blurry image when we scale it up.
// So we make the size hint a part of the cache key.
// This might lead to a lot of extra entries for the same SVG file,
// which is potentially wasteful of RAM, but better that than blurry images.
Some(size_hint)
} else {
// For other images we just use one cache value, no matter what the size we render at.
None
};
let mut cache = self.cache.lock();
if let Some(handle) = cache.get(&(Cow::Borrowed(uri), texture_options)) {
let texture = SizedTexture::from_handle(handle);
let bucket = cache
.entry(PrimaryKey {
uri: uri.to_owned(),
texture_options,
})
.or_default();
if let Some(texture) = bucket.get(&svg_size_hint) {
texture
.last_used
.store(self.pass_index.load(Relaxed), Relaxed);
let texture = SizedTexture::new(texture.handle.id(), texture.source_size);
Ok(TexturePoll::Ready { texture })
} else {
match ctx.try_load_image(uri, size_hint)? {
ImagePoll::Pending { size } => Ok(TexturePoll::Pending { size }),
ImagePoll::Ready { image } => {
let source_size = image.source_size;
let handle = ctx.load_texture(uri, image, texture_options);
let texture = SizedTexture::from_handle(&handle);
cache.insert((Cow::Owned(uri.to_owned()), texture_options), handle);
let texture = SizedTexture::new(handle.id(), source_size);
bucket.insert(
svg_size_hint,
Entry {
last_used: AtomicU64::new(self.pass_index.load(Relaxed)),
source_size,
handle,
},
);
let reduce_texture_memory = ctx.options(|o| o.reduce_texture_memory);
if reduce_texture_memory {
let loaders = ctx.loaders();
@@ -54,7 +105,7 @@ impl TextureLoader for DefaultTextureLoader {
#[cfg(feature = "log")]
log::trace!("forget {uri:?}");
self.cache.lock().retain(|(u, _), _| u != uri);
self.cache.lock().retain(|key, _value| key.uri != uri);
}
fn forget_all(&self) {
@@ -64,13 +115,35 @@ impl TextureLoader for DefaultTextureLoader {
self.cache.lock().clear();
}
fn end_pass(&self, _: usize) {}
fn end_pass(&self, pass_index: u64) {
self.pass_index.store(pass_index, Relaxed);
let mut cache = self.cache.lock();
cache.retain(|_key, bucket| {
if 2 <= bucket.len() {
// There are multiple textures of the same URI (e.g. SVGs of different scales).
// This could be because someone has an SVG in a resizable container,
// and so we get a lot of different sizes of it.
// This could wast VRAM, so we remove the ones that are not used in this frame.
bucket.retain(|_, texture| pass_index <= texture.last_used.load(Relaxed) + 1);
}
!bucket.is_empty()
});
}
fn byte_size(&self) -> usize {
self.cache
.lock()
.values()
.map(|texture| texture.byte_size())
.map(|bucket| {
bucket
.values()
.map(|texture| texture.handle.byte_size())
.sum::<usize>()
})
.sum()
}
}
fn is_svg(uri: &str) -> bool {
uri.ends_with(".svg")
}

View File

@@ -87,15 +87,6 @@ pub struct Memory {
#[cfg_attr(feature = "persistence", serde(skip))]
pub(crate) viewport_id: ViewportId,
/// Which popup-window is open (if any)?
/// Could be a combo box, color picker, menu, etc.
/// Optionally stores the position of the popup (usually this would be the position where
/// the user clicked).
/// If position is [`None`], the popup position will be calculated based on some configuration
/// (e.g. relative to some other widget).
#[cfg_attr(feature = "persistence", serde(skip))]
popup: Option<OpenPopup>,
#[cfg_attr(feature = "persistence", serde(skip))]
everything_is_visible: bool,
@@ -116,6 +107,15 @@ pub struct Memory {
#[cfg_attr(feature = "persistence", serde(skip))]
pub(crate) focus: ViewportIdMap<Focus>,
/// Which popup-window is open on a viewport (if any)?
/// Could be a combo box, color picker, menu, etc.
/// Optionally stores the position of the popup (usually this would be the position where
/// the user clicked).
/// If position is [`None`], the popup position will be calculated based on some configuration
/// (e.g. relative to some other widget).
#[cfg_attr(feature = "persistence", serde(skip))]
popups: ViewportIdMap<OpenPopup>,
}
impl Default for Memory {
@@ -130,7 +130,7 @@ impl Default for Memory {
viewport_id: Default::default(),
areas: Default::default(),
to_global: Default::default(),
popup: Default::default(),
popups: Default::default(),
everything_is_visible: Default::default(),
add_fonts: Default::default(),
};
@@ -284,14 +284,6 @@ pub struct Options {
/// By default this is `true` in debug builds.
pub warn_on_id_clash: bool,
// ------------------------------
// Input:
/// Multiplier for the scroll speed when reported in [`crate::MouseWheelUnit::Line`]s.
pub line_scroll_speed: f32,
/// Controls the speed at which we zoom in when doing ctrl/cmd + scroll.
pub scroll_zoom_speed: f32,
/// Options related to input state handling.
pub input_options: crate::input_state::InputOptions,
@@ -311,14 +303,6 @@ pub struct Options {
impl Default for Options {
fn default() -> Self {
// TODO(emilk): figure out why these constants need to be different on web and on native (winit).
let is_web = cfg!(target_arch = "wasm32");
let line_scroll_speed = if is_web {
8.0
} else {
40.0 // Scroll speed decided by consensus: https://github.com/emilk/egui/issues/461
};
Self {
dark_style: std::sync::Arc::new(Theme::Dark.default_style()),
light_style: std::sync::Arc::new(Theme::Light.default_style()),
@@ -335,8 +319,6 @@ impl Default for Options {
warn_on_id_clash: cfg!(debug_assertions),
// Input:
line_scroll_speed,
scroll_zoom_speed: 1.0 / 200.0,
input_options: Default::default(),
reduce_texture_memory: false,
}
@@ -391,9 +373,6 @@ impl Options {
screen_reader: _, // needs to come from the integration
preload_font_glyphs: _,
warn_on_id_clash,
line_scroll_speed,
scroll_zoom_speed,
input_options,
reduce_texture_memory,
} = self;
@@ -448,22 +427,6 @@ impl Options {
CollapsingHeader::new("🖱 Input")
.default_open(false)
.show(ui, |ui| {
ui.horizontal(|ui| {
ui.label("Line scroll speed");
ui.add(crate::DragValue::new(line_scroll_speed).range(0.0..=f32::INFINITY))
.on_hover_text(
"How many lines to scroll with each tick of the mouse wheel",
);
});
ui.horizontal(|ui| {
ui.label("Scroll zoom speed");
ui.add(
crate::DragValue::new(scroll_zoom_speed)
.range(0.0..=f32::INFINITY)
.speed(0.001),
)
.on_hover_text("How fast to zoom with ctrl/cmd + scroll");
});
input_options.ui(ui);
});
@@ -790,6 +753,7 @@ impl Memory {
// Cleanup
self.interactions.retain(|id, _| viewports.contains(id));
self.areas.retain(|id, _| viewports.contains(id));
self.popups.retain(|id, _| viewports.contains(id));
self.areas.entry(self.viewport_id).or_default();
@@ -809,11 +773,11 @@ impl Memory {
self.focus_mut().end_pass(used_ids);
// Clean up abandoned popups.
if let Some(popup) = &mut self.popup {
if let Some(popup) = self.popups.get_mut(&self.viewport_id) {
if popup.open_this_frame {
popup.open_this_frame = false;
} else {
self.popup = None;
self.popups.remove(&self.viewport_id);
}
}
}
@@ -992,59 +956,6 @@ impl Memory {
self.focus_mut().focused_widget = None;
}
/// Is any widget being dragged?
#[deprecated = "Use `Context::dragged_id` instead"]
#[inline(always)]
pub fn is_anything_being_dragged(&self) -> bool {
self.interaction().potential_drag_id.is_some()
}
/// Is this specific widget being dragged?
///
/// A widget that sense both clicks and drags is only marked as "dragged"
/// when the mouse has moved a bit, but `is_being_dragged` will return true immediately.
#[deprecated = "Use `Context::is_being_dragged` instead"]
#[inline(always)]
pub fn is_being_dragged(&self, id: Id) -> bool {
self.interaction().potential_drag_id == Some(id)
}
/// Get the id of the widget being dragged, if any.
///
/// Note that this is set as soon as the mouse is pressed,
/// so the widget may not yet be marked as "dragged",
/// as that can only happen after the mouse has moved a bit
/// (at least if the widget is interesated in both clicks and drags).
#[deprecated = "Use `Context::dragged_id` instead"]
#[inline(always)]
pub fn dragged_id(&self) -> Option<Id> {
self.interaction().potential_drag_id
}
/// Set which widget is being dragged.
#[inline(always)]
#[deprecated = "Use `Context::set_dragged_id` instead"]
pub fn set_dragged_id(&mut self, id: Id) {
self.interaction_mut().potential_drag_id = Some(id);
}
/// Stop dragging any widget.
#[inline(always)]
#[deprecated = "Use `Context::stop_dragging` instead"]
pub fn stop_dragging(&mut self) {
self.interaction_mut().potential_drag_id = None;
}
/// Is something else being dragged?
///
/// Returns true if we are dragging something, but not the given widget.
#[inline(always)]
#[deprecated = "Use `Context::dragging_something_else` instead"]
pub fn dragging_something_else(&self, not_this: Id) -> bool {
let drag_id = self.interaction().potential_drag_id;
drag_id.is_some() && drag_id != Some(not_this)
}
/// Forget window positions, sizes etc.
/// Can be used to auto-layout windows.
pub fn reset_areas(&mut self) {
@@ -1107,19 +1018,23 @@ impl OpenPopup {
impl Memory {
/// Is the given popup open?
pub fn is_popup_open(&self, popup_id: Id) -> bool {
self.popup.is_some_and(|state| state.id == popup_id) || self.everything_is_visible()
self.popups
.get(&self.viewport_id)
.is_some_and(|state| state.id == popup_id)
|| self.everything_is_visible()
}
/// Is any popup open?
pub fn any_popup_open(&self) -> bool {
self.popup.is_some() || self.everything_is_visible()
self.popups.contains_key(&self.viewport_id) || self.everything_is_visible()
}
/// 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.
pub fn open_popup(&mut self, popup_id: Id) {
self.popup = Some(OpenPopup::new(popup_id, None));
self.popups
.insert(self.viewport_id, OpenPopup::new(popup_id, None));
}
/// Popups must call this every frame while open.
@@ -1128,7 +1043,7 @@ impl Memory {
/// called. For example, when a context menu is open and the underlying widget stops
/// being rendered.
pub fn keep_popup_open(&mut self, popup_id: Id) {
if let Some(state) = self.popup.as_mut() {
if let Some(state) = self.popups.get_mut(&self.viewport_id) {
if state.id == popup_id {
state.open_this_frame = true;
}
@@ -1137,18 +1052,20 @@ impl Memory {
/// Open the popup and remember its position.
pub fn open_popup_at(&mut self, popup_id: Id, pos: impl Into<Option<Pos2>>) {
self.popup = Some(OpenPopup::new(popup_id, pos.into()));
self.popups
.insert(self.viewport_id, OpenPopup::new(popup_id, pos.into()));
}
/// Get the position for this popup.
pub fn popup_position(&self, id: Id) -> Option<Pos2> {
self.popup
self.popups
.get(&self.viewport_id)
.and_then(|state| if state.id == id { state.pos } else { None })
}
/// Close any currently open popup.
pub fn close_all_popups(&mut self) {
self.popup = None;
self.popups.clear();
}
/// Close the given popup, if it is open.
@@ -1156,7 +1073,7 @@ impl Memory {
/// See also [`Self::close_all_popups`] if you want to close any / all currently open popups.
pub fn close_popup(&mut self, popup_id: Id) {
if self.is_popup_open(popup_id) {
self.popup = None;
self.popups.remove(&self.viewport_id);
}
}
@@ -1318,7 +1235,7 @@ impl Areas {
self.visible_areas_current_frame.insert(layer_id);
self.wants_to_be_on_top.insert(layer_id);
if !self.order.iter().any(|x| *x == layer_id) {
if !self.order.contains(&layer_id) {
self.order.push(layer_id);
}
}
@@ -1339,10 +1256,10 @@ impl Areas {
self.sublayers.entry(parent).or_default().insert(child);
// Make sure the layers are in the order list:
if !self.order.iter().any(|x| *x == parent) {
if !self.order.contains(&parent) {
self.order.push(parent);
}
if !self.order.iter().any(|x| *x == child) {
if !self.order.contains(&child) {
self.order.push(child);
}
}
@@ -1351,7 +1268,7 @@ impl Areas {
self.order
.iter()
.filter(|layer| layer.order == order && !self.is_sublayer(layer))
.last()
.next_back()
.copied()
}

View File

@@ -1,5 +1,5 @@
#![allow(deprecated)]
//! Menu bar functionality (very basic so far).
//! Deprecated menu API - Use [`crate::containers::menu`] instead.
//!
//! Usage:
//! ```
@@ -23,8 +23,8 @@ use super::{
use crate::{
epaint, vec2,
widgets::{Button, ImageButton},
Align2, Area, Color32, Frame, Key, LayerId, Layout, NumExt, Order, Stroke, Style, TextWrapMode,
UiKind, WidgetText,
Align2, Area, Color32, Frame, Key, LayerId, Layout, NumExt as _, Order, Stroke, Style,
TextWrapMode, UiKind, WidgetText,
};
use epaint::mutex::RwLock;
use std::sync::Arc;
@@ -88,6 +88,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"]
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

@@ -1,5 +1,5 @@
/// An `enum` of common operating systems.
#[allow(clippy::upper_case_acronyms)] // `Ios` looks too ugly
#[expect(clippy::upper_case_acronyms)] // `Ios` looks too ugly
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum OperatingSystem {
/// Unknown OS - could be wasm
@@ -76,4 +76,9 @@ impl OperatingSystem {
Self::Unknown
}
}
/// Are we either macOS or iOS?
pub fn is_mac(&self) -> bool {
matches!(self, Self::Mac | Self::IOS)
}
}

View File

@@ -295,7 +295,7 @@ impl Painter {
/// ## Debug painting
impl Painter {
#[allow(clippy::needless_pass_by_value)]
#[expect(clippy::needless_pass_by_value)]
pub fn debug_rect(&self, rect: Rect, color: Color32, text: impl ToString) {
self.rect(
rect,
@@ -321,7 +321,7 @@ impl Painter {
/// Text with a background.
///
/// See also [`Context::debug_text`].
#[allow(clippy::needless_pass_by_value)]
#[expect(clippy::needless_pass_by_value)]
pub fn debug_text(
&self,
pos: Pos2,
@@ -498,7 +498,7 @@ impl Painter {
/// [`Self::layout`] or [`Self::layout_no_wrap`].
///
/// Returns where the text ended up.
#[allow(clippy::needless_pass_by_value)]
#[expect(clippy::needless_pass_by_value)]
pub fn text(
&self,
pos: Pos2,
@@ -583,16 +583,6 @@ impl Painter {
));
}
}
#[deprecated = "Use `Painter::galley` or `Painter::galley_with_override_text_color` instead"]
#[inline]
pub fn galley_with_color(&self, pos: Pos2, galley: Arc<Galley>, text_color: Color32) {
if !galley.is_empty() {
self.add(Shape::galley_with_override_text_color(
pos, galley, text_color,
));
}
}
}
fn tint_shape_towards(shape: &mut Shape, target: Color32) {

View File

@@ -3,7 +3,7 @@ use ahash::HashMap;
use crate::{id::IdSet, style, Align, Id, IdMap, LayerId, Rangef, Rect, Vec2, WidgetRects};
#[cfg(debug_assertions)]
use crate::{pos2, Align2, Color32, FontId, NumExt, Painter};
use crate::{pos2, Align2, Color32, FontId, NumExt as _, Painter};
/// Reset at the start of each frame.
#[derive(Clone, Debug, Default)]

View File

@@ -218,6 +218,14 @@ impl Response {
&& self.ctx.input(|i| i.pointer.button_triple_clicked(button))
}
/// Was this widget middle-clicked or clicked while holding down a modifier key?
///
/// This is used by [`crate::Hyperlink`] to check if a URL should be opened
/// in a new tab, using [`crate::OpenUrl::new_tab`].
pub fn clicked_with_open_in_background(&self) -> bool {
self.middle_clicked() || self.clicked() && self.ctx.input(|i| i.modifiers.any())
}
/// `true` if there was a click *outside* the rect of this widget.
///
/// Clicks on widgets contained in this one counts as clicks inside this widget,
@@ -387,19 +395,6 @@ impl Response {
self.drag_stopped() && self.ctx.input(|i| i.pointer.button_released(button))
}
/// The widget was being dragged, but now it has been released.
#[inline]
#[deprecated = "Renamed 'drag_stopped'"]
pub fn drag_released(&self) -> bool {
self.drag_stopped()
}
/// The widget was being dragged by the button, but now it has been released.
#[deprecated = "Renamed 'drag_stopped_by'"]
pub fn drag_released_by(&self, button: PointerButton) -> bool {
self.drag_stopped_by(button)
}
/// If dragged, how many points were we dragged and in what direction?
#[inline]
pub fn drag_delta(&self) -> Vec2 {

View File

@@ -747,7 +747,8 @@ impl ScrollStyle {
// ----------------------------------------------------------------------------
/// Scroll animation configuration, used when programmatically scrolling somewhere (e.g. with `[crate::Ui::scroll_to_cursor]`)
/// Scroll animation configuration, used when programmatically scrolling somewhere (e.g. with `[crate::Ui::scroll_to_cursor]`).
///
/// The animation duration is calculated based on the distance to be scrolled via `[ScrollAnimation::points_per_second]`
/// and can be clamped to a min / max duration via `[ScrollAnimation::duration]`.
#[derive(Copy, Clone, Debug, PartialEq)]
@@ -1271,7 +1272,7 @@ pub fn default_text_styles() -> BTreeMap<TextStyle, FontId> {
impl Default for Style {
fn default() -> Self {
#[allow(deprecated)]
#[expect(deprecated)]
Self {
override_font_id: None,
override_text_style: None,
@@ -1574,7 +1575,7 @@ use crate::{
impl Style {
pub fn ui(&mut self, ui: &mut crate::Ui) {
#[allow(deprecated)]
#[expect(deprecated)]
let Self {
override_font_id,
override_text_style,

View File

@@ -45,7 +45,7 @@ pub fn update_accesskit_for_text_widget(
let row_id = parent_id.with(row_index);
ctx.accesskit_node_builder(row_id, |builder| {
builder.set_role(accesskit::Role::TextRun);
let rect = global_from_galley * row.rect();
let rect = global_from_galley * row.rect_without_leading_space();
builder.set_bounds(accesskit::Rect {
x0: rect.min.x.into(),
y0: rect.min.y.into(),

View File

@@ -530,7 +530,7 @@ impl LabelSelectionState {
let mut cursor_state = self.cursor_for(ui, response, global_from_galley, galley);
let old_range = cursor_state.char_range();
let old_range = cursor_state.range(galley);
if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() {
if response.contains_pointer() {
@@ -544,7 +544,7 @@ impl LabelSelectionState {
}
}
if let Some(mut cursor_range) = cursor_state.char_range() {
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);
@@ -562,7 +562,7 @@ impl LabelSelectionState {
}
// Look for changes due to keyboard and/or mouse interaction:
let new_range = cursor_state.char_range();
let new_range = cursor_state.range(galley);
let selection_changed = old_range != new_range;
if let (true, Some(range)) = (selection_changed, new_range) {
@@ -616,7 +616,7 @@ impl LabelSelectionState {
let old_primary = old_selection.map(|s| s.primary);
let new_primary = self.selection.as_ref().map(|s| s.primary);
if let Some(new_primary) = new_primary {
let primary_changed = old_primary.map_or(true, |old| {
let primary_changed = old_primary.is_none_or(|old| {
old.widget_id != new_primary.widget_id || old.ccursor != new_primary.ccursor
});
if primary_changed && new_primary.widget_id == widget_id {
@@ -632,7 +632,7 @@ impl LabelSelectionState {
}
}
let cursor_range = cursor_state.char_range();
let cursor_range = cursor_state.range(galley);
let mut new_vertex_indices = vec![];

View File

@@ -1,8 +1,9 @@
//! Text cursor changes/interaction, without modifying the text.
use epaint::text::{cursor::CCursor, Galley};
use unicode_segmentation::UnicodeSegmentation as _;
use crate::{epaint, NumExt, Rect, Response, Ui};
use crate::{epaint, NumExt as _, Rect, Response, Ui};
use super::CCursorRange;
@@ -34,6 +35,16 @@ impl TextCursorState {
self.ccursor_range
}
/// The currently selected range of characters, clamped within the character
/// range of the given [`Galley`].
pub fn range(&self, galley: &Galley) -> Option<CCursorRange> {
self.ccursor_range.map(|mut range| {
range.primary = galley.clamp_cursor(&range.primary);
range.secondary = galley.clamp_cursor(&range.secondary);
range
})
}
/// Sets the currently selected range of characters.
pub fn set_char_range(&mut self, ccursor_range: Option<CCursorRange>) {
self.ccursor_range = ccursor_range;
@@ -68,7 +79,7 @@ impl TextCursorState {
if response.hovered() && ui.input(|i| i.pointer.any_pressed()) {
// The start of a drag (or a click).
if ui.input(|i| i.modifiers.shift) {
if let Some(mut cursor_range) = self.char_range() {
if let Some(mut cursor_range) = self.range(galley) {
cursor_range.primary = cursor_at_pointer;
self.set_char_range(Some(cursor_range));
} else {
@@ -80,7 +91,7 @@ impl TextCursorState {
true
} else if is_being_dragged {
// Drag to select text:
if let Some(mut cursor_range) = self.char_range() {
if let Some(mut cursor_range) = self.range(galley) {
cursor_range.primary = cursor_at_pointer;
self.set_char_range(Some(cursor_range));
}
@@ -166,7 +177,7 @@ fn select_line_at(text: &str, ccursor: CCursor) -> CCursorRange {
pub fn ccursor_next_word(text: &str, ccursor: CCursor) -> CCursor {
CCursor {
index: next_word_boundary_char_index(text.chars(), ccursor.index),
index: next_word_boundary_char_index(text, ccursor.index),
prefer_next_row: false,
}
}
@@ -180,9 +191,10 @@ fn ccursor_next_line(text: &str, ccursor: CCursor) -> CCursor {
pub fn ccursor_previous_word(text: &str, ccursor: CCursor) -> CCursor {
let num_chars = text.chars().count();
let reversed: String = text.graphemes(true).rev().collect();
CCursor {
index: num_chars
- next_word_boundary_char_index(text.chars().rev(), num_chars - ccursor.index),
- next_word_boundary_char_index(&reversed, num_chars - ccursor.index).min(num_chars),
prefer_next_row: true,
}
}
@@ -196,22 +208,25 @@ fn ccursor_previous_line(text: &str, ccursor: CCursor) -> CCursor {
}
}
fn next_word_boundary_char_index(it: impl Iterator<Item = char>, mut index: usize) -> usize {
let mut it = it.skip(index);
if let Some(_first) = it.next() {
index += 1;
if let Some(second) = it.next() {
index += 1;
for next in it {
if is_word_char(next) != is_word_char(second) {
break;
}
index += 1;
}
fn next_word_boundary_char_index(text: &str, index: usize) -> usize {
for word in text.split_word_bound_indices() {
// Splitting considers contiguous whitespace as one word, such words must be skipped,
// this handles cases for example ' abc' (a space and a word), the cursor is at the beginning
// (before space) - this jumps at the end of 'abc' (this is consistent with text editors
// or browsers)
let ci = char_index_from_byte_index(text, word.0);
if ci > index && !skip_word(word.1) {
return ci;
}
}
index
char_index_from_byte_index(text, text.len())
}
fn skip_word(text: &str) -> bool {
// skip words that contain anything other than alphanumeric characters and underscore
// (i.e. whitespace, dashes, etc.)
!text.chars().any(|c| !is_word_char(c))
}
fn next_line_boundary_char_index(it: impl Iterator<Item = char>, mut index: usize) -> usize {
@@ -233,7 +248,7 @@ fn next_line_boundary_char_index(it: impl Iterator<Item = char>, mut index: usiz
}
pub fn is_word_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_'
c.is_alphanumeric() || c == '_'
}
fn is_linebreak(c: char) -> bool {
@@ -270,6 +285,16 @@ pub fn byte_index_from_char_index(s: &str, char_index: usize) -> usize {
s.len()
}
pub fn char_index_from_byte_index(input: &str, byte_index: usize) -> usize {
for (ci, (bi, _)) in input.char_indices().enumerate() {
if bi == byte_index {
return ci;
}
}
input.char_indices().last().map_or(0, |(i, _)| i + 1)
}
pub fn slice_char_range(s: &str, char_range: std::ops::Range<usize>) -> &str {
assert!(
char_range.start <= char_range.end,
@@ -293,3 +318,38 @@ pub fn cursor_rect(galley: &Galley, cursor: &CCursor, row_height: f32) -> Rect {
cursor_pos
}
#[cfg(test)]
mod test {
use crate::text_selection::text_cursor_state::next_word_boundary_char_index;
#[test]
fn test_next_word_boundary_char_index() {
// ASCII only
let text = "abc d3f g_h i-j";
assert_eq!(next_word_boundary_char_index(text, 1), 3);
assert_eq!(next_word_boundary_char_index(text, 3), 7);
assert_eq!(next_word_boundary_char_index(text, 9), 11);
assert_eq!(next_word_boundary_char_index(text, 12), 13);
assert_eq!(next_word_boundary_char_index(text, 13), 15);
assert_eq!(next_word_boundary_char_index(text, 15), 15);
assert_eq!(next_word_boundary_char_index("", 0), 0);
assert_eq!(next_word_boundary_char_index("", 1), 0);
// Unicode graphemes, some of which consist of multiple Unicode characters,
// !!! Unicode character is not always what is tranditionally considered a character,
// the values below are correct despite not seeming that way on the first look,
// handling of and around emojis is kind of weird and is not consistent across
// text editors and browsers
let text = "❤️👍 skvělá knihovna 👍❤️";
assert_eq!(next_word_boundary_char_index(text, 0), 2);
assert_eq!(next_word_boundary_char_index(text, 2), 3); // this does not skip the space between thumbs-up and 'skvělá'
assert_eq!(next_word_boundary_char_index(text, 6), 10);
assert_eq!(next_word_boundary_char_index(text, 9), 10);
assert_eq!(next_word_boundary_char_index(text, 12), 19);
assert_eq!(next_word_boundary_char_index(text, 15), 19);
assert_eq!(next_word_boundary_char_index(text, 19), 20);
assert_eq!(next_word_boundary_char_index(text, 20), 21);
}
}

View File

@@ -25,10 +25,10 @@ use crate::{
color_picker, Button, Checkbox, DragValue, Hyperlink, Image, ImageSource, Label, Link,
RadioButton, SelectableLabel, Separator, Spinner, TextEdit, Widget,
},
Align, Color32, Context, CursorIcon, DragAndDrop, Id, InnerResponse, InputState, LayerId,
Memory, Order, Painter, PlatformOutput, Pos2, Rangef, Rect, Response, Rgba, RichText, Sense,
Style, TextStyle, TextWrapMode, UiBuilder, UiKind, UiStack, UiStackInfo, Vec2, WidgetRect,
WidgetText,
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,
};
// ----------------------------------------------------------------------------
@@ -98,7 +98,7 @@ pub struct Ui {
sizing_pass: bool,
/// Indicates whether this Ui belongs to a Menu.
#[allow(deprecated)]
#[expect(deprecated)]
menu_state: Option<Arc<RwLock<crate::menu::MenuState>>>,
/// The [`UiStack`] for this [`Ui`].
@@ -666,7 +666,7 @@ impl Ui {
///
/// This is determined first by [`Style::wrap_mode`], and then by the layout of this [`Ui`].
pub fn wrap_mode(&self) -> TextWrapMode {
#[allow(deprecated)]
#[expect(deprecated)]
if let Some(wrap_mode) = self.style.wrap_mode {
wrap_mode
}
@@ -2055,8 +2055,8 @@ impl Ui {
/// ```
#[must_use = "You should check if the user clicked this with `if ui.button(…).clicked() { … } "]
#[inline]
pub fn button(&mut self, text: impl Into<WidgetText>) -> Response {
Button::new(text).ui(self)
pub fn button<'a>(&mut self, atoms: impl IntoAtoms<'a>) -> Response {
Button::new(atoms).ui(self)
}
/// A button as small as normal body text.
@@ -2073,8 +2073,8 @@ impl Ui {
///
/// See also [`Self::toggle_value`].
#[inline]
pub fn checkbox(&mut self, checked: &mut bool, text: impl Into<WidgetText>) -> Response {
Checkbox::new(checked, text).ui(self)
pub fn checkbox<'a>(&mut self, checked: &'a mut bool, atoms: impl IntoAtoms<'a>) -> Response {
Checkbox::new(checked, atoms).ui(self)
}
/// Acts like a checkbox, but looks like a [`SelectableLabel`].
@@ -2095,8 +2095,8 @@ impl Ui {
/// Often you want to use [`Self::radio_value`] instead.
#[must_use = "You should check if the user clicked this with `if ui.radio(…).clicked() { … } "]
#[inline]
pub fn radio(&mut self, selected: bool, text: impl Into<WidgetText>) -> Response {
RadioButton::new(selected, text).ui(self)
pub fn radio<'a>(&mut self, selected: bool, atoms: impl IntoAtoms<'a>) -> Response {
RadioButton::new(selected, atoms).ui(self)
}
/// Show a [`RadioButton`]. It is selected if `*current_value == selected_value`.
@@ -2118,13 +2118,13 @@ impl Ui {
/// }
/// # });
/// ```
pub fn radio_value<Value: PartialEq>(
pub fn radio_value<'a, Value: PartialEq>(
&mut self,
current_value: &mut Value,
alternative: Value,
text: impl Into<WidgetText>,
atoms: impl IntoAtoms<'a>,
) -> Response {
let mut response = self.radio(*current_value == alternative, text);
let mut response = self.radio(*current_value == alternative, atoms);
if response.clicked() && *current_value != alternative {
*current_value = alternative;
response.mark_changed();
@@ -3015,7 +3015,7 @@ impl Ui {
self.close_kind(UiKind::Menu);
}
#[allow(deprecated)]
#[expect(deprecated)]
pub(crate) fn set_menu_state(
&mut self,
menu_state: Option<Arc<RwLock<crate::menu::MenuState>>>,
@@ -3041,15 +3041,15 @@ impl Ui {
/// ```
///
/// See also: [`Self::close`] and [`Response::context_menu`].
pub fn menu_button<R>(
pub fn menu_button<'a, R>(
&mut self,
title: impl Into<WidgetText>,
atoms: impl IntoAtoms<'a>,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<Option<R>> {
let (response, inner) = if menu::is_in_menu(self) {
menu::SubMenuButton::new(title).ui(self, add_contents)
menu::SubMenuButton::new(atoms).ui(self, add_contents)
} else {
menu::MenuButton::new(title).ui(self, add_contents)
menu::MenuButton::new(atoms).ui(self, add_contents)
};
InnerResponse::new(inner.map(|i| i.inner), response)
}
@@ -3156,7 +3156,7 @@ impl Drop for Ui {
/// Show this rectangle to the user if certain debug options are set.
#[cfg(debug_assertions)]
fn register_rect(ui: &Ui, rect: Rect) {
use emath::{Align2, GuiRounding};
use emath::{Align2, GuiRounding as _};
let debug = ui.style().debug;

View File

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

View File

@@ -258,7 +258,7 @@ impl UiStack {
// these methods act on the entire stack
impl UiStack {
/// Return an iterator that walks the stack from this node to the root.
#[allow(clippy::iter_without_into_iter)]
#[expect(clippy::iter_without_into_iter)]
pub fn iter(&self) -> UiStackIterator<'_> {
UiStackIterator { next: Some(self) }
}

View File

@@ -467,7 +467,7 @@ impl IdTypeMap {
/// For tests
#[cfg(feature = "persistence")]
#[allow(unused)]
#[allow(unused, clippy::allow_attributes)]
fn get_generation<T: SerializableAny>(&self, id: Id) -> Option<usize> {
let element = self.map.get(&hash(TypeId::of::<T>(), id))?;
match element {

View File

@@ -42,7 +42,7 @@
//! a [`ViewportCommand`] to it using [`Context::send_viewport_cmd`].
//! You can interact with other viewports using [`Context::send_viewport_cmd_to`].
//!
//! There is an example in <https://github.com/emilk/egui/tree/master/examples/multiple_viewports/src/main.rs>.
//! There is an example in <https://github.com/emilk/egui/tree/main/examples/multiple_viewports/src/main.rs>.
//!
//! You can find all available viewports in [`crate::RawInput::viewports`] and the active viewport in
//! [`crate::InputState::viewport`]:
@@ -260,7 +260,6 @@ pub type ImmediateViewportRendererCallback = dyn for<'a> Fn(&Context, ImmediateV
/// The default values are implementation defined, so you may want to explicitly
/// configure the size of the window, and what buttons are shown.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
#[allow(clippy::option_option)]
pub struct ViewportBuilder {
/// The title of the viewport.
/// `eframe` will use this as the title of the native window.
@@ -295,6 +294,7 @@ pub struct ViewportBuilder {
pub title_shown: Option<bool>,
pub titlebar_buttons_shown: Option<bool>,
pub titlebar_shown: Option<bool>,
pub has_shadow: Option<bool>,
// windows:
pub drag_and_drop: Option<bool>,
@@ -381,6 +381,10 @@ impl ViewportBuilder {
/// The default is `false`.
/// If this is not working, it's because the graphic context doesn't support transparency,
/// you will need to set the transparency in the eframe!
///
/// ## Platform-specific
///
/// **macOS:** When using this feature to create an overlay-like UI, you likely want to combine this with [`Self::with_has_shadow`] set to `false` in order to avoid ghosting artifacts.
#[inline]
pub fn with_transparent(mut self, transparent: bool) -> Self {
self.transparent = Some(transparent);
@@ -434,7 +438,6 @@ 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.
#[inline]
pub fn with_movable_by_background(mut self, value: bool) -> Self {
@@ -463,6 +466,19 @@ impl ViewportBuilder {
self
}
/// macOS: Set to `false` to make the window render without a drop shadow.
///
/// The default is `true`.
///
/// Disabling this feature can solve ghosting issues experienced if using [`Self::with_transparent`].
///
/// Look at winit for more details
#[inline]
pub fn with_has_shadow(mut self, has_shadow: bool) -> Self {
self.has_shadow = Some(has_shadow);
self
}
/// windows: Whether show or hide the window icon in the taskbar.
#[inline]
pub fn with_taskbar(mut self, show: bool) -> Self {
@@ -654,6 +670,7 @@ impl ViewportBuilder {
title_shown: new_title_shown,
titlebar_buttons_shown: new_titlebar_buttons_shown,
titlebar_shown: new_titlebar_shown,
has_shadow: new_has_shadow,
close_button: new_close_button,
minimize_button: new_minimize_button,
maximize_button: new_maximize_button,
@@ -824,6 +841,11 @@ impl ViewportBuilder {
recreate_window = true;
}
if new_has_shadow.is_some() && self.has_shadow != new_has_shadow {
self.has_shadow = new_has_shadow;
recreate_window = true;
}
if new_taskbar.is_some() && self.taskbar != new_taskbar {
self.taskbar = new_taskbar;
recreate_window = true;

View File

@@ -1,6 +1,7 @@
use std::{borrow::Cow, sync::Arc};
use emath::GuiRounding as _;
use epaint::text::TextFormat;
use std::fmt::Formatter;
use std::{borrow::Cow, sync::Arc};
use crate::{
text::{LayoutJob, TextWrapping},
@@ -488,7 +489,16 @@ impl RichText {
/// which will be replaced with a color chosen by the widget that paints the text.
#[derive(Clone)]
pub enum WidgetText {
RichText(RichText),
/// Plain unstyled text.
///
/// We have this as a special case, as it is the common-case,
/// and it uses less memory than [`Self::RichText`].
Text(String),
/// Text and optional style choices for it.
///
/// Prefer [`Self::Text`] if there is no styling, as it will be faster.
RichText(Arc<RichText>),
/// Use this [`LayoutJob`] when laying out the text.
///
@@ -502,7 +512,7 @@ pub enum WidgetText {
///
/// You can color the text however you want, or use [`Color32::PLACEHOLDER`]
/// which will be replaced with a color chosen by the widget that paints the text.
LayoutJob(LayoutJob),
LayoutJob(Arc<LayoutJob>),
/// Use exactly this galley when painting the text.
///
@@ -511,9 +521,21 @@ pub enum WidgetText {
Galley(Arc<Galley>),
}
impl std::fmt::Debug for WidgetText {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let text = self.text();
match self {
Self::Text(_) => write!(f, "Text({text:?})"),
Self::RichText(_) => write!(f, "RichText({text:?})"),
Self::LayoutJob(_) => write!(f, "LayoutJob({text:?})"),
Self::Galley(_) => write!(f, "Galley({text:?})"),
}
}
}
impl Default for WidgetText {
fn default() -> Self {
Self::RichText(RichText::default())
Self::Text(String::new())
}
}
@@ -521,6 +543,7 @@ impl WidgetText {
#[inline]
pub fn is_empty(&self) -> bool {
match self {
Self::Text(text) => text.is_empty(),
Self::RichText(text) => text.is_empty(),
Self::LayoutJob(job) => job.is_empty(),
Self::Galley(galley) => galley.is_empty(),
@@ -530,21 +553,36 @@ impl WidgetText {
#[inline]
pub fn text(&self) -> &str {
match self {
Self::Text(text) => text,
Self::RichText(text) => text.text(),
Self::LayoutJob(job) => &job.text,
Self::Galley(galley) => galley.text(),
}
}
/// Map the contents based on the provided closure.
///
/// - [`Self::Text`] => convert to [`RichText`] and call f
/// - [`Self::RichText`] => call f
/// - else do nothing
#[must_use]
fn map_rich_text<F>(self, f: F) -> Self
where
F: FnOnce(RichText) -> RichText,
{
match self {
Self::Text(text) => Self::RichText(Arc::new(f(RichText::new(text)))),
Self::RichText(text) => Self::RichText(Arc::new(f(Arc::unwrap_or_clone(text)))),
other => other,
}
}
/// Override the [`TextStyle`] if, and only if, this is a [`RichText`].
///
/// Prefer using [`RichText`] directly!
#[inline]
pub fn text_style(self, text_style: TextStyle) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.text_style(text_style)),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
self.map_rich_text(|text| text.text_style(text_style))
}
/// Set the [`TextStyle`] unless it has already been set
@@ -552,10 +590,7 @@ impl WidgetText {
/// Prefer using [`RichText`] directly!
#[inline]
pub fn fallback_text_style(self, text_style: TextStyle) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.fallback_text_style(text_style)),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
self.map_rich_text(|text| text.fallback_text_style(text_style))
}
/// Override text color if, and only if, this is a [`RichText`].
@@ -563,111 +598,85 @@ impl WidgetText {
/// Prefer using [`RichText`] directly!
#[inline]
pub fn color(self, color: impl Into<Color32>) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.color(color)),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
self.map_rich_text(|text| text.color(color))
}
/// Prefer using [`RichText`] directly!
#[inline]
pub fn heading(self) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.heading()),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
self.map_rich_text(|text| text.heading())
}
/// Prefer using [`RichText`] directly!
#[inline]
pub fn monospace(self) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.monospace()),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
self.map_rich_text(|text| text.monospace())
}
/// Prefer using [`RichText`] directly!
#[inline]
pub fn code(self) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.code()),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
self.map_rich_text(|text| text.code())
}
/// Prefer using [`RichText`] directly!
#[inline]
pub fn strong(self) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.strong()),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
self.map_rich_text(|text| text.strong())
}
/// Prefer using [`RichText`] directly!
#[inline]
pub fn weak(self) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.weak()),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
self.map_rich_text(|text| text.weak())
}
/// Prefer using [`RichText`] directly!
#[inline]
pub fn underline(self) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.underline()),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
self.map_rich_text(|text| text.underline())
}
/// Prefer using [`RichText`] directly!
#[inline]
pub fn strikethrough(self) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.strikethrough()),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
self.map_rich_text(|text| text.strikethrough())
}
/// Prefer using [`RichText`] directly!
#[inline]
pub fn italics(self) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.italics()),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
self.map_rich_text(|text| text.italics())
}
/// Prefer using [`RichText`] directly!
#[inline]
pub fn small(self) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.small()),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
self.map_rich_text(|text| text.small())
}
/// Prefer using [`RichText`] directly!
#[inline]
pub fn small_raised(self) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.small_raised()),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
self.map_rich_text(|text| text.small_raised())
}
/// Prefer using [`RichText`] directly!
#[inline]
pub fn raised(self) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.raised()),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
self.map_rich_text(|text| text.raised())
}
/// Prefer using [`RichText`] directly!
#[inline]
pub fn background_color(self, background_color: impl Into<Color32>) -> Self {
match self {
Self::RichText(text) => Self::RichText(text.background_color(background_color)),
Self::LayoutJob(_) | Self::Galley(_) => self,
}
self.map_rich_text(|text| text.background_color(background_color))
}
/// Returns a value rounded to [`emath::GUI_ROUNDING`].
pub(crate) fn font_height(&self, fonts: &epaint::Fonts, style: &Style) -> f32 {
match self {
Self::Text(_) => fonts.row_height(&FontSelection::Default.resolve(style)),
Self::RichText(text) => text.font_height(fonts, style),
Self::LayoutJob(job) => job.font_height(fonts),
Self::Galley(galley) => {
@@ -685,11 +694,24 @@ impl WidgetText {
style: &Style,
fallback_font: FontSelection,
default_valign: Align,
) -> LayoutJob {
) -> Arc<LayoutJob> {
match self {
Self::RichText(text) => text.into_layout_job(style, fallback_font, default_valign),
Self::Text(text) => Arc::new(LayoutJob::simple_format(
text,
TextFormat {
font_id: FontSelection::Default.resolve(style),
color: crate::Color32::PLACEHOLDER,
valign: default_valign,
..Default::default()
},
)),
Self::RichText(text) => Arc::new(Arc::unwrap_or_clone(text).into_layout_job(
style,
fallback_font,
default_valign,
)),
Self::LayoutJob(job) => job,
Self::Galley(galley) => (*galley.job).clone(),
Self::Galley(galley) => galley.job.clone(),
}
}
@@ -721,12 +743,30 @@ impl WidgetText {
default_valign: Align,
) -> Arc<Galley> {
match self {
Self::RichText(text) => {
let mut layout_job = text.into_layout_job(style, fallback_font, default_valign);
Self::Text(text) => {
let mut layout_job = LayoutJob::simple_format(
text,
TextFormat {
font_id: FontSelection::Default.resolve(style),
color: crate::Color32::PLACEHOLDER,
valign: default_valign,
..Default::default()
},
);
layout_job.wrap = text_wrapping;
ctx.fonts(|f| f.layout_job(layout_job))
}
Self::LayoutJob(mut job) => {
Self::RichText(text) => {
let mut layout_job = Arc::unwrap_or_clone(text).into_layout_job(
style,
fallback_font,
default_valign,
);
layout_job.wrap = text_wrapping;
ctx.fonts(|f| f.layout_job(layout_job))
}
Self::LayoutJob(job) => {
let mut job = Arc::unwrap_or_clone(job);
job.wrap = text_wrapping;
ctx.fonts(|f| f.layout_job(job))
}
@@ -738,48 +778,55 @@ impl WidgetText {
impl From<&str> for WidgetText {
#[inline]
fn from(text: &str) -> Self {
Self::RichText(RichText::new(text))
Self::Text(text.to_owned())
}
}
impl From<&String> for WidgetText {
#[inline]
fn from(text: &String) -> Self {
Self::RichText(RichText::new(text))
Self::Text(text.clone())
}
}
impl From<String> for WidgetText {
#[inline]
fn from(text: String) -> Self {
Self::RichText(RichText::new(text))
Self::Text(text)
}
}
impl From<&Box<str>> for WidgetText {
#[inline]
fn from(text: &Box<str>) -> Self {
Self::RichText(RichText::new(text.clone()))
Self::Text(text.to_string())
}
}
impl From<Box<str>> for WidgetText {
#[inline]
fn from(text: Box<str>) -> Self {
Self::RichText(RichText::new(text))
Self::Text(text.into())
}
}
impl From<Cow<'_, str>> for WidgetText {
#[inline]
fn from(text: Cow<'_, str>) -> Self {
Self::RichText(RichText::new(text))
Self::Text(text.into_owned())
}
}
impl From<RichText> for WidgetText {
#[inline]
fn from(rich_text: RichText) -> Self {
Self::RichText(Arc::new(rich_text))
}
}
impl From<Arc<RichText>> for WidgetText {
#[inline]
fn from(rich_text: Arc<RichText>) -> Self {
Self::RichText(rich_text)
}
}
@@ -787,6 +834,13 @@ impl From<RichText> for WidgetText {
impl From<LayoutJob> for WidgetText {
#[inline]
fn from(layout_job: LayoutJob) -> Self {
Self::LayoutJob(Arc::new(layout_job))
}
}
impl From<Arc<LayoutJob>> for WidgetText {
#[inline]
fn from(layout_job: Arc<LayoutJob>) -> Self {
Self::LayoutJob(layout_job)
}
}
@@ -797,3 +851,13 @@ impl From<Arc<Galley>> for WidgetText {
Self::Galley(galley)
}
}
#[cfg(test)]
mod tests {
use crate::WidgetText;
#[test]
fn ensure_small_widget_text() {
assert_eq!(size_of::<WidgetText>(), size_of::<String>());
}
}

View File

@@ -1,6 +1,7 @@
use crate::{
widgets, Align, Color32, CornerRadius, FontSelection, Image, NumExt, Rect, Response, Sense,
Stroke, TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetType,
Atom, AtomExt as _, AtomKind, AtomLayout, AtomLayoutResponse, Color32, CornerRadius, Frame,
Image, IntoAtoms, NumExt as _, Response, Sense, Stroke, TextWrapMode, Ui, Vec2, Widget,
WidgetInfo, WidgetText, WidgetType,
};
/// Clickable button with text.
@@ -23,58 +24,66 @@ use crate::{
/// ```
#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
pub struct Button<'a> {
image: Option<Image<'a>>,
text: Option<WidgetText>,
right_text: WidgetText,
wrap_mode: Option<TextWrapMode>,
/// None means default for interact
layout: AtomLayout<'a>,
fill: Option<Color32>,
stroke: Option<Stroke>,
sense: Sense,
small: bool,
frame: Option<bool>,
min_size: Vec2,
corner_radius: Option<CornerRadius>,
selected: bool,
image_tint_follows_text_color: bool,
limit_image_size: bool,
}
impl<'a> Button<'a> {
pub fn new(text: impl Into<WidgetText>) -> Self {
Self::opt_image_and_text(None, Some(text.into()))
}
/// Creates a button with an image. The size of the image as displayed is defined by the provided size.
#[allow(clippy::needless_pass_by_value)]
pub fn image(image: impl Into<Image<'a>>) -> Self {
Self::opt_image_and_text(Some(image.into()), None)
}
/// Creates a button with an image to the left of the text. The size of the image as displayed is defined by the provided size.
#[allow(clippy::needless_pass_by_value)]
pub fn image_and_text(image: impl Into<Image<'a>>, text: impl Into<WidgetText>) -> Self {
Self::opt_image_and_text(Some(image.into()), Some(text.into()))
}
pub fn opt_image_and_text(image: Option<Image<'a>>, text: Option<WidgetText>) -> Self {
pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
Self {
text,
image,
right_text: Default::default(),
wrap_mode: None,
layout: AtomLayout::new(atoms.into_atoms()).sense(Sense::click()),
fill: None,
stroke: None,
sense: Sense::click(),
small: false,
frame: None,
min_size: Vec2::ZERO,
corner_radius: None,
selected: false,
image_tint_follows_text_color: false,
limit_image_size: false,
}
}
/// 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
/// (using [`crate::AtomExt::atom_max_height_font_size`]).
pub fn image(image: impl Into<Image<'a>>) -> Self {
Self::opt_image_and_text(Some(image.into()), None)
}
/// Creates a button with an image to the left of the text.
///
/// Note: In contrast to [`Button::new`], this limits the image size to the default font height
/// (using [`crate::AtomExt::atom_max_height_font_size`]).
pub fn image_and_text(image: impl Into<Image<'a>>, text: impl Into<WidgetText>) -> Self {
Self::opt_image_and_text(Some(image.into()), Some(text.into()))
}
/// Create a button with an optional image and optional text.
///
/// Note: In contrast to [`Button::new`], this limits the image size to the default font height
/// (using [`crate::AtomExt::atom_max_height_font_size`]).
pub fn opt_image_and_text(image: Option<Image<'a>>, text: Option<WidgetText>) -> Self {
let mut button = Self::new(());
if let Some(image) = image {
button.layout.push_right(image);
}
if let Some(text) = text {
button.layout.push_right(text);
}
button.limit_image_size = true;
button
}
/// Set the wrap mode for the text.
///
/// By default, [`crate::Ui::wrap_mode`] will be used, which can be overridden with [`crate::Style::wrap_mode`].
@@ -82,23 +91,20 @@ impl<'a> Button<'a> {
/// Note that any `\n` in the text will always produce a new line.
#[inline]
pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self {
self.wrap_mode = Some(wrap_mode);
self.layout = self.layout.wrap_mode(wrap_mode);
self
}
/// Set [`Self::wrap_mode`] to [`TextWrapMode::Wrap`].
#[inline]
pub fn wrap(mut self) -> Self {
self.wrap_mode = Some(TextWrapMode::Wrap);
self
pub fn wrap(self) -> Self {
self.wrap_mode(TextWrapMode::Wrap)
}
/// Set [`Self::wrap_mode`] to [`TextWrapMode::Truncate`].
#[inline]
pub fn truncate(mut self) -> Self {
self.wrap_mode = Some(TextWrapMode::Truncate);
self
pub fn truncate(self) -> Self {
self.wrap_mode(TextWrapMode::Truncate)
}
/// Override background fill color. Note that this will override any on-hover effects.
@@ -106,7 +112,6 @@ impl<'a> Button<'a> {
#[inline]
pub fn fill(mut self, fill: impl Into<Color32>) -> Self {
self.fill = Some(fill.into());
self.frame = Some(true);
self
}
@@ -122,9 +127,6 @@ impl<'a> Button<'a> {
/// Make this a small button, suitable for embedding into text.
#[inline]
pub fn small(mut self) -> Self {
if let Some(text) = self.text {
self.text = Some(text.text_style(TextStyle::Body));
}
self.small = true;
self
}
@@ -140,7 +142,7 @@ impl<'a> Button<'a> {
/// Change this to a drag-button with `Sense::drag()`.
#[inline]
pub fn sense(mut self, sense: Sense) -> Self {
self.sense = sense;
self.layout = self.layout.sense(sense);
self
}
@@ -184,15 +186,22 @@ impl<'a> Button<'a> {
///
/// See also [`Self::right_text`].
#[inline]
pub fn shortcut_text(mut self, shortcut_text: impl Into<WidgetText>) -> Self {
self.right_text = shortcut_text.into().weak();
pub fn shortcut_text(mut self, shortcut_text: impl Into<Atom<'a>>) -> Self {
let mut atom = shortcut_text.into();
atom.kind = match atom.kind {
AtomKind::Text(text) => AtomKind::Text(text.weak()),
other => other,
};
self.layout.push_right(Atom::grow());
self.layout.push_right(atom);
self
}
/// Show some text on the right side of the button.
#[inline]
pub fn right_text(mut self, right_text: impl Into<WidgetText>) -> Self {
self.right_text = right_text.into();
pub fn right_text(mut self, right_text: impl Into<Atom<'a>>) -> Self {
self.layout.push_right(Atom::grow());
self.layout.push_right(right_text.into());
self
}
@@ -202,39 +211,41 @@ impl<'a> Button<'a> {
self.selected = selected;
self
}
}
impl Widget for Button<'_> {
fn ui(self, ui: &mut Ui) -> Response {
/// Show the button and return a [`AtomLayoutResponse`] for painting custom contents.
pub fn atom_ui(self, ui: &mut Ui) -> AtomLayoutResponse {
let Button {
text,
image,
right_text,
wrap_mode,
mut layout,
fill,
stroke,
sense,
small,
frame,
min_size,
mut min_size,
corner_radius,
selected,
image_tint_follows_text_color,
limit_image_size,
} = self;
let frame = frame.unwrap_or_else(|| ui.visuals().button_frame);
if !small {
min_size.y = min_size.y.at_least(ui.spacing().interact_size.y);
}
let default_font_height = || {
let font_selection = FontSelection::default();
let font_id = font_selection.resolve(ui.style());
ui.fonts(|f| f.row_height(&font_id))
};
if limit_image_size {
layout.map_atoms(|atom| {
if matches!(&atom.kind, AtomKind::Image(_)) {
atom.atom_max_height_font_size(ui)
} else {
atom
}
});
}
let text_font_height = ui
.fonts(|fonts| text.as_ref().map(|wt| wt.font_height(fonts, ui.style())))
.unwrap_or_else(default_font_height);
let text = layout.text().map(String::from);
let mut button_padding = if frame {
let has_frame = frame.unwrap_or_else(|| ui.visuals().button_frame);
let mut button_padding = if has_frame {
ui.spacing().button_padding
} else {
Vec2::ZERO
@@ -243,188 +254,53 @@ impl Widget for Button<'_> {
button_padding.y = 0.0;
}
let (space_available_for_image, right_text_font_height) = if let Some(text) = &text {
let font_height = ui.fonts(|fonts| text.font_height(fonts, ui.style()));
(
Vec2::splat(font_height), // Reasonable?
font_height,
)
let mut prepared = layout
.frame(Frame::new().inner_margin(button_padding))
.min_size(min_size)
.allocate(ui);
let response = if ui.is_rect_visible(prepared.response.rect) {
let visuals = ui.style().interact_selectable(&prepared.response, selected);
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 {
let stroke = stroke.unwrap_or(visuals.bg_stroke);
let fill = fill.unwrap_or(visuals.weak_bg_fill);
prepared.frame = prepared
.frame
.inner_margin(
button_padding + Vec2::splat(visuals.expansion) - Vec2::splat(stroke.width),
)
.outer_margin(-Vec2::splat(visuals.expansion))
.fill(fill)
.stroke(stroke)
.corner_radius(corner_radius.unwrap_or(visuals.corner_radius));
};
prepared.paint(ui)
} else {
(
ui.available_size() - 2.0 * button_padding,
default_font_height(),
)
AtomLayoutResponse::empty(prepared.response)
};
let image_size = if let Some(image) = &image {
image
.load_and_calc_size(ui, space_available_for_image)
.unwrap_or(space_available_for_image)
} else {
Vec2::ZERO
};
let gap_before_right_text = ui.spacing().item_spacing.x;
let mut text_wrap_width = ui.available_width() - 2.0 * button_padding.x;
if image.is_some() {
text_wrap_width -= image_size.x + ui.spacing().icon_spacing;
}
// Note: we don't wrap the right text
let right_galley = (!right_text.is_empty()).then(|| {
right_text.into_galley(
ui,
Some(TextWrapMode::Extend),
f32::INFINITY,
TextStyle::Button,
)
});
if let Some(right_galley) = &right_galley {
// Leave space for the right text:
text_wrap_width -= gap_before_right_text + right_galley.size().x;
}
let galley =
text.map(|text| text.into_galley(ui, wrap_mode, text_wrap_width, TextStyle::Button));
let mut desired_size = Vec2::ZERO;
if image.is_some() {
desired_size.x += image_size.x;
desired_size.y = desired_size.y.max(image_size.y);
}
if image.is_some() && galley.is_some() {
desired_size.x += ui.spacing().icon_spacing;
}
if let Some(galley) = &galley {
desired_size.x += galley.size().x;
desired_size.y = desired_size.y.max(galley.size().y).max(text_font_height);
}
if let Some(right_galley) = &right_galley {
desired_size.x += gap_before_right_text + right_galley.size().x;
desired_size.y = desired_size
.y
.max(right_galley.size().y)
.max(right_text_font_height);
}
desired_size += 2.0 * button_padding;
if !small {
desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y);
}
desired_size = desired_size.at_least(min_size);
let (rect, mut response) = ui.allocate_at_least(desired_size, sense);
response.widget_info(|| {
if let Some(galley) = &galley {
WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), galley.text())
response.response.widget_info(|| {
if let Some(text) = &text {
WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), text)
} else {
WidgetInfo::new(WidgetType::Button)
}
});
if ui.is_rect_visible(rect) {
let visuals = ui.style().interact(&response);
let (frame_expansion, frame_cr, frame_fill, frame_stroke) = if selected {
let selection = ui.visuals().selection;
(
Vec2::ZERO,
CornerRadius::ZERO,
selection.bg_fill,
selection.stroke,
)
} else if frame {
let expansion = Vec2::splat(visuals.expansion);
(
expansion,
visuals.corner_radius,
visuals.weak_bg_fill,
visuals.bg_stroke,
)
} else {
Default::default()
};
let frame_cr = corner_radius.unwrap_or(frame_cr);
let frame_fill = fill.unwrap_or(frame_fill);
let frame_stroke = stroke.unwrap_or(frame_stroke);
ui.painter().rect(
rect.expand2(frame_expansion),
frame_cr,
frame_fill,
frame_stroke,
epaint::StrokeKind::Inside,
);
let mut cursor_x = rect.min.x + button_padding.x;
if let Some(image) = &image {
let mut image_pos = ui
.layout()
.align_size_within_rect(image_size, rect.shrink2(button_padding))
.min;
if galley.is_some() || right_galley.is_some() {
image_pos.x = cursor_x;
}
let image_rect = Rect::from_min_size(image_pos, image_size);
cursor_x += image_size.x;
let tlr = image.load_for_size(ui.ctx(), image_size);
let mut image_options = image.image_options().clone();
if image_tint_follows_text_color {
image_options.tint = image_options.tint * visuals.text_color();
}
widgets::image::paint_texture_load_result(
ui,
&tlr,
image_rect,
image.show_loading_spinner,
&image_options,
None,
);
response = widgets::image::texture_load_result_response(
&image.source(ui.ctx()),
&tlr,
response,
);
}
if image.is_some() && galley.is_some() {
cursor_x += ui.spacing().icon_spacing;
}
if let Some(galley) = galley {
let mut text_pos = ui
.layout()
.align_size_within_rect(galley.size(), rect.shrink2(button_padding))
.min;
if image.is_some() || right_galley.is_some() {
text_pos.x = cursor_x;
}
ui.painter().galley(text_pos, galley, visuals.text_color());
}
if let Some(right_galley) = right_galley {
// Always align to the right
let layout = if ui.layout().is_horizontal() {
ui.layout().with_main_align(Align::Max)
} else {
ui.layout().with_cross_align(Align::Max)
};
let right_text_pos = layout
.align_size_within_rect(right_galley.size(), rect.shrink2(button_padding))
.min;
ui.painter()
.galley(right_text_pos, right_galley, visuals.text_color());
}
}
if let Some(cursor) = ui.visuals().interact_cursor {
if response.hovered() {
ui.ctx().set_cursor_icon(cursor);
}
}
response
}
}
impl Widget for Button<'_> {
fn ui(self, ui: &mut Ui) -> Response {
self.atom_ui(ui).response
}
}

View File

@@ -1,6 +1,6 @@
use crate::{
epaint, pos2, vec2, NumExt, Response, Sense, Shape, TextStyle, Ui, Vec2, Widget, WidgetInfo,
WidgetText, WidgetType,
epaint, pos2, Atom, AtomLayout, Atoms, Id, IntoAtoms, NumExt as _, Response, Sense, Shape, Ui,
Vec2, Widget, WidgetInfo, WidgetType,
};
// TODO(emilk): allow checkbox without a text label
@@ -19,21 +19,21 @@ use crate::{
#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
pub struct Checkbox<'a> {
checked: &'a mut bool,
text: WidgetText,
atoms: Atoms<'a>,
indeterminate: bool,
}
impl<'a> Checkbox<'a> {
pub fn new(checked: &'a mut bool, text: impl Into<WidgetText>) -> Self {
pub fn new(checked: &'a mut bool, atoms: impl IntoAtoms<'a>) -> Self {
Checkbox {
checked,
text: text.into(),
atoms: atoms.into_atoms(),
indeterminate: false,
}
}
pub fn without_text(checked: &'a mut bool) -> Self {
Self::new(checked, WidgetText::default())
Self::new(checked, ())
}
/// Display an indeterminate state (neither checked nor unchecked)
@@ -51,92 +51,88 @@ impl Widget for Checkbox<'_> {
fn ui(self, ui: &mut Ui) -> Response {
let Checkbox {
checked,
text,
mut atoms,
indeterminate,
} = self;
let spacing = &ui.spacing();
let icon_width = spacing.icon_width;
let icon_spacing = spacing.icon_spacing;
let (galley, mut desired_size) = if text.is_empty() {
(None, vec2(icon_width, 0.0))
} else {
let total_extra = vec2(icon_width + icon_spacing, 0.0);
let mut min_size = Vec2::splat(spacing.interact_size.y);
min_size.y = min_size.y.at_least(icon_width);
let wrap_width = ui.available_width() - total_extra.x;
let galley = text.into_galley(ui, None, wrap_width, TextStyle::Button);
// In order to center the checkbox based on min_size we set the icon height to at least min_size.y
let mut icon_size = Vec2::splat(icon_width);
icon_size.y = icon_size.y.at_least(min_size.y);
let rect_id = Id::new("egui::checkbox");
atoms.push_left(Atom::custom(rect_id, icon_size));
let mut desired_size = total_extra + galley.size();
desired_size = desired_size.at_least(spacing.interact_size);
let text = atoms.text().map(String::from);
(Some(galley), desired_size)
};
let mut prepared = AtomLayout::new(atoms)
.sense(Sense::click())
.min_size(min_size)
.allocate(ui);
desired_size = desired_size.at_least(Vec2::splat(spacing.interact_size.y));
desired_size.y = desired_size.y.max(icon_width);
let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click());
if response.clicked() {
if prepared.response.clicked() {
*checked = !*checked;
response.mark_changed();
prepared.response.mark_changed();
}
response.widget_info(|| {
prepared.response.widget_info(|| {
if indeterminate {
WidgetInfo::labeled(
WidgetType::Checkbox,
ui.is_enabled(),
galley.as_ref().map_or("", |x| x.text()),
text.as_deref().unwrap_or(""),
)
} else {
WidgetInfo::selected(
WidgetType::Checkbox,
ui.is_enabled(),
*checked,
galley.as_ref().map_or("", |x| x.text()),
text.as_deref().unwrap_or(""),
)
}
});
if ui.is_rect_visible(rect) {
if ui.is_rect_visible(prepared.response.rect) {
// let visuals = ui.style().interact_selectable(&response, *checked); // too colorful
let visuals = ui.style().interact(&response);
let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect);
ui.painter().add(epaint::RectShape::new(
big_icon_rect.expand(visuals.expansion),
visuals.corner_radius,
visuals.bg_fill,
visuals.bg_stroke,
epaint::StrokeKind::Inside,
));
let visuals = *ui.style().interact(&prepared.response);
prepared.fallback_text_color = visuals.text_color();
let response = prepared.paint(ui);
if indeterminate {
// Horizontal line:
ui.painter().add(Shape::hline(
small_icon_rect.x_range(),
small_icon_rect.center().y,
visuals.fg_stroke,
));
} else if *checked {
// Check mark:
ui.painter().add(Shape::line(
vec![
pos2(small_icon_rect.left(), small_icon_rect.center().y),
pos2(small_icon_rect.center().x, small_icon_rect.bottom()),
pos2(small_icon_rect.right(), small_icon_rect.top()),
],
visuals.fg_stroke,
if let Some(rect) = response.rect(rect_id) {
let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect);
ui.painter().add(epaint::RectShape::new(
big_icon_rect.expand(visuals.expansion),
visuals.corner_radius,
visuals.bg_fill,
visuals.bg_stroke,
epaint::StrokeKind::Inside,
));
if indeterminate {
// Horizontal line:
ui.painter().add(Shape::hline(
small_icon_rect.x_range(),
small_icon_rect.center().y,
visuals.fg_stroke,
));
} else if *checked {
// Check mark:
ui.painter().add(Shape::line(
vec![
pos2(small_icon_rect.left(), small_icon_rect.center().y),
pos2(small_icon_rect.center().x, small_icon_rect.bottom()),
pos2(small_icon_rect.right(), small_icon_rect.top()),
],
visuals.fg_stroke,
));
}
}
if let Some(galley) = galley {
let text_pos = pos2(
rect.min.x + icon_width + icon_spacing,
rect.center().y - 0.5 * galley.size().y,
);
ui.painter().galley(text_pos, galley, visuals.text_color());
}
response.response
} else {
prepared.response
}
response
}
}

View File

@@ -3,7 +3,7 @@
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, WidgetInfo, WidgetType,
Sense, Ui, UiKind, Widget as _, WidgetInfo, WidgetType,
};
use epaint::{
ecolor::{Color32, Hsva, HsvaGamma, Rgba},

View File

@@ -3,8 +3,8 @@
use std::{cmp::Ordering, ops::RangeInclusive};
use crate::{
emath, text, Button, CursorIcon, Key, Modifiers, NumExt, Response, RichText, Sense, TextEdit,
TextWrapMode, Ui, Widget, WidgetInfo, MINUS_CHAR_STR,
emath, text, Button, CursorIcon, Id, Key, Modifiers, NumExt as _, Response, RichText, Sense,
TextEdit, TextWrapMode, Ui, Widget, WidgetInfo, MINUS_CHAR_STR,
};
// ----------------------------------------------------------------------------
@@ -550,7 +550,7 @@ impl Widget for DragValue<'_> {
}
// some clones below are redundant if AccessKit is disabled
#[allow(clippy::redundant_clone)]
#[expect(clippy::redundant_clone)]
let mut response = if is_kb_editing {
let mut value_text = ui
.data_mut(|data| data.remove_temp::<String>(id))
@@ -569,6 +569,11 @@ impl Widget for DragValue<'_> {
.font(text_style),
);
// Select all text when the edit gains focus.
if ui.memory_mut(|mem| mem.gained_focus(id)) {
select_all_text(ui, id, response.id, &value_text);
}
let update = if update_while_editing {
// Update when the edit content has changed.
response.changed()
@@ -623,12 +628,7 @@ impl Widget for DragValue<'_> {
if response.clicked() {
ui.data_mut(|data| data.remove::<String>(id));
ui.memory_mut(|mem| mem.request_focus(id));
let mut state = TextEdit::load_state(ui.ctx(), id).unwrap_or_default();
state.cursor.set_char_range(Some(text::CCursorRange::two(
text::CCursor::default(),
text::CCursor::new(value_text.chars().count()),
)));
state.store(ui.ctx(), response.id);
select_all_text(ui, id, response.id, &value_text);
} else if response.dragged() {
ui.ctx().set_cursor_icon(cursor_icon);
@@ -759,6 +759,16 @@ pub(crate) fn clamp_value_to_range(x: f64, range: RangeInclusive<f64>) -> f64 {
}
}
/// Select all text in the `DragValue` text edit widget.
fn select_all_text(ui: &Ui, widget_id: Id, response_id: Id, value_text: &str) {
let mut state = TextEdit::load_state(ui.ctx(), widget_id).unwrap_or_default();
state.cursor.set_char_range(Some(text::CCursorRange::two(
text::CCursor::default(),
text::CCursor::new(value_text.chars().count()),
)));
state.store(ui.ctx(), response_id);
}
#[cfg(test)]
mod tests {
use super::clamp_value_to_range;

View File

@@ -96,7 +96,7 @@ pub struct Hyperlink {
}
impl Hyperlink {
#[allow(clippy::needless_pass_by_value)]
#[expect(clippy::needless_pass_by_value)]
pub fn new(url: impl ToString) -> Self {
let url = url.to_string();
Self {
@@ -106,7 +106,7 @@ impl Hyperlink {
}
}
#[allow(clippy::needless_pass_by_value)]
#[expect(clippy::needless_pass_by_value)]
pub fn from_label_and_url(text: impl Into<WidgetText>, url: impl ToString) -> Self {
Self {
url: url.to_string(),
@@ -129,18 +129,16 @@ impl Widget for Hyperlink {
let response = ui.add(Link::new(text));
if response.clicked() {
let modifiers = ui.ctx().input(|i| i.modifiers);
ui.ctx().open_url(crate::OpenUrl {
url: url.clone(),
new_tab: new_tab || modifiers.any(),
});
}
if response.middle_clicked() {
if response.clicked_with_open_in_background() {
ui.ctx().open_url(crate::OpenUrl {
url: url.clone(),
new_tab: true,
});
} else if response.clicked() {
ui.ctx().open_url(crate::OpenUrl {
url: url.clone(),
new_tab,
});
}
if ui.style().url_in_tooltip {

View File

@@ -1,6 +1,6 @@
use std::{borrow::Cow, slice::Iter, sync::Arc, time::Duration};
use emath::{Align, Float as _, Rot2};
use emath::{Align, Float as _, GuiRounding as _, NumExt as _, Rot2};
use epaint::{
text::{LayoutJob, TextFormat, TextWrapping},
RectShape,
@@ -54,7 +54,7 @@ pub struct Image<'a> {
sense: Sense,
size: ImageSize,
pub(crate) show_loading_spinner: Option<bool>,
alt_text: Option<String>,
pub(crate) alt_text: Option<String>,
}
impl<'a> Image<'a> {
@@ -156,6 +156,9 @@ impl<'a> Image<'a> {
/// Fit the image to its original size with some scaling.
///
/// The texel size of the source image will be multiplied by the `scale` factor,
/// and then become the _ui_ size of the [`Image`].
///
/// This will cause the image to overflow if it is larger than the available space.
///
/// If [`Image::max_size`] is set, this is guaranteed to never exceed that limit.
@@ -291,9 +294,9 @@ impl<'a, T: Into<ImageSource<'a>>> From<T> for Image<'a> {
impl<'a> Image<'a> {
/// Returns the size the image will occupy in the final UI.
#[inline]
pub fn calc_size(&self, available_size: Vec2, original_image_size: Option<Vec2>) -> Vec2 {
let original_image_size = original_image_size.unwrap_or(Vec2::splat(24.0)); // Fallback for still-loading textures, or failure to load.
self.size.calc_size(available_size, original_image_size)
pub fn calc_size(&self, available_size: Vec2, image_source_size: Option<Vec2>) -> Vec2 {
let image_source_size = image_source_size.unwrap_or(Vec2::splat(24.0)); // Fallback for still-loading textures, or failure to load.
self.size.calc_size(available_size, image_source_size)
}
pub fn load_and_calc_size(&self, ui: &Ui, available_size: Vec2) -> Option<Vec2> {
@@ -373,9 +376,27 @@ impl<'a> Image<'a> {
/// ```
#[inline]
pub fn paint_at(&self, ui: &Ui, rect: Rect) {
let pixels_per_point = ui.pixels_per_point();
let rect = rect.round_to_pixels(pixels_per_point);
// Load exactly the size of the rectangle we are painting to.
// This is important for getting crisp SVG:s.
let pixel_size = (pixels_per_point * rect.size()).round();
let texture = self.source(ui.ctx()).clone().load(
ui.ctx(),
self.texture_options,
SizeHint::Size {
width: pixel_size.x as _,
height: pixel_size.y as _,
maintain_aspect_ratio: false, // no - just get exactly what we asked for
},
);
paint_texture_load_result(
ui,
&self.load_for_size(ui.ctx(), rect.size()),
&texture,
rect,
self.show_loading_spinner,
&self.image_options,
@@ -387,8 +408,8 @@ impl<'a> Image<'a> {
impl Widget for Image<'_> {
fn ui(self, ui: &mut Ui) -> Response {
let tlr = self.load_for_size(ui.ctx(), ui.available_size());
let original_image_size = tlr.as_ref().ok().and_then(|t| t.size());
let ui_size = self.calc_size(ui.available_size(), original_image_size);
let image_source_size = tlr.as_ref().ok().and_then(|t| t.size());
let ui_size = self.calc_size(ui.available_size(), image_source_size);
let (rect, response) = ui.allocate_exact_size(ui_size, self.sense);
response.widget_info(|| {
@@ -440,7 +461,10 @@ pub struct ImageSize {
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum ImageFit {
/// Fit the image to its original size, scaled by some factor.
/// Fit the image to its original srce size, scaled by some factor.
///
/// The original size of the image is usually its texel resolution,
/// but for an SVG it's the point size of the SVG.
///
/// Ignores how much space is actually available in the ui.
Original { scale: f32 },
@@ -467,25 +491,38 @@ impl ImageFit {
impl ImageSize {
/// Size hint for e.g. rasterizing an svg.
pub fn hint(&self, available_size: Vec2, pixels_per_point: f32) -> SizeHint {
let size = match self.fit {
ImageFit::Original { scale } => return SizeHint::Scale(scale.ord()),
let Self {
maintain_aspect_ratio,
max_size,
fit,
} = *self;
let point_size = match fit {
ImageFit::Original { scale } => {
return SizeHint::Scale((pixels_per_point * scale).ord())
}
ImageFit::Fraction(fract) => available_size * fract,
ImageFit::Exact(size) => size,
};
let size = size.min(self.max_size);
let size = size * pixels_per_point;
let point_size = point_size.at_most(max_size);
let pixel_size = pixels_per_point * point_size;
// `inf` on an axis means "any value"
match (size.x.is_finite(), size.y.is_finite()) {
(true, true) => SizeHint::Size(size.x.round() as u32, size.y.round() as u32),
(true, false) => SizeHint::Width(size.x.round() as u32),
(false, true) => SizeHint::Height(size.y.round() as u32),
match (pixel_size.x.is_finite(), pixel_size.y.is_finite()) {
(true, true) => SizeHint::Size {
width: pixel_size.x.round() as u32,
height: pixel_size.y.round() as u32,
maintain_aspect_ratio,
},
(true, false) => SizeHint::Width(pixel_size.x.round() as u32),
(false, true) => SizeHint::Height(pixel_size.y.round() as u32),
(false, false) => SizeHint::Scale(pixels_per_point.ord()),
}
}
/// Calculate the final on-screen size in points.
pub fn calc_size(&self, available_size: Vec2, original_image_size: Vec2) -> Vec2 {
pub fn calc_size(&self, available_size: Vec2, image_source_size: Vec2) -> Vec2 {
let Self {
maintain_aspect_ratio,
max_size,
@@ -493,7 +530,7 @@ impl ImageSize {
} = *self;
match fit {
ImageFit::Original { scale } => {
let image_size = original_image_size * scale;
let image_size = scale * image_source_size;
if image_size.x <= max_size.x && image_size.y <= max_size.y {
image_size
} else {
@@ -502,11 +539,11 @@ impl ImageSize {
}
ImageFit::Fraction(fract) => {
let scale_to_size = (available_size * fract).min(max_size);
scale_to_fit(original_image_size, scale_to_size, maintain_aspect_ratio)
scale_to_fit(image_source_size, scale_to_size, maintain_aspect_ratio)
}
ImageFit::Exact(size) => {
let scale_to_size = size.min(max_size);
scale_to_fit(original_image_size, scale_to_size, maintain_aspect_ratio)
scale_to_fit(image_source_size, scale_to_size, maintain_aspect_ratio)
}
}
}

View File

@@ -93,10 +93,10 @@ impl Widget for ImageButton<'_> {
let available_size_for_image = ui.available_size() - 2.0 * padding;
let tlr = self.image.load_for_size(ui.ctx(), available_size_for_image);
let original_image_size = tlr.as_ref().ok().and_then(|t| t.size());
let image_source_size = tlr.as_ref().ok().and_then(|t| t.size());
let image_size = self
.image
.calc_size(available_size_for_image, original_image_size);
.calc_size(available_size_for_image, image_source_size);
let padded_size = image_size + 2.0 * padding;
let (rect, response) = ui.allocate_exact_size(padded_size, self.sense);

View File

@@ -1,12 +1,10 @@
use std::sync::Arc;
use crate::{
epaint, pos2, text_selection, Align, Direction, FontSelection, Galley, Pos2, Response, Sense,
Stroke, TextWrapMode, Ui, Widget, WidgetInfo, WidgetText, WidgetType,
epaint, pos2, text_selection::LabelSelectionState, Align, Direction, FontSelection, Galley,
Pos2, Response, Sense, Stroke, TextWrapMode, Ui, Widget, WidgetInfo, WidgetText, WidgetType,
};
use self::text_selection::LabelSelectionState;
/// Static text.
///
/// Usually it is more convenient to use [`Ui::label`].
@@ -182,9 +180,11 @@ impl Label {
}
let valign = ui.text_valign();
let mut layout_job = self
.text
.into_layout_job(ui.style(), FontSelection::Default, valign);
let mut layout_job = Arc::unwrap_or_clone(self.text.into_layout_job(
ui.style(),
FontSelection::Default,
valign,
));
let available_width = ui.available_width();
@@ -216,7 +216,9 @@ impl Label {
let pos = pos2(ui.max_rect().left(), ui.cursor().top());
assert!(!galley.rows.is_empty(), "Galleys are never empty");
// collect a response from many rows:
let rect = galley.rows[0].rect().translate(pos.to_vec2());
let rect = galley.rows[0]
.rect_without_leading_space()
.translate(pos.to_vec2());
let mut response = ui.allocate_rect(rect, sense);
for placed_row in galley.rows.iter().skip(1) {
let rect = placed_row.rect().translate(pos.to_vec2());

View File

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

View File

@@ -1,6 +1,6 @@
use crate::{
epaint, pos2, vec2, NumExt, Response, Sense, TextStyle, Ui, Vec2, Widget, WidgetInfo,
WidgetText, WidgetType,
epaint, Atom, AtomLayout, Atoms, Id, IntoAtoms, NumExt as _, Response, Sense, Ui, Vec2, Widget,
WidgetInfo, WidgetType,
};
/// One out of several alternatives, either selected or not.
@@ -23,89 +23,84 @@ use crate::{
/// # });
/// ```
#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
pub struct RadioButton {
pub struct RadioButton<'a> {
checked: bool,
text: WidgetText,
atoms: Atoms<'a>,
}
impl RadioButton {
pub fn new(checked: bool, text: impl Into<WidgetText>) -> Self {
impl<'a> RadioButton<'a> {
pub fn new(checked: bool, atoms: impl IntoAtoms<'a>) -> Self {
Self {
checked,
text: text.into(),
atoms: atoms.into_atoms(),
}
}
}
impl Widget for RadioButton {
impl Widget for RadioButton<'_> {
fn ui(self, ui: &mut Ui) -> Response {
let Self { checked, text } = self;
let Self { checked, mut atoms } = self;
let spacing = &ui.spacing();
let icon_width = spacing.icon_width;
let icon_spacing = spacing.icon_spacing;
let (galley, mut desired_size) = if text.is_empty() {
(None, vec2(icon_width, 0.0))
} else {
let total_extra = vec2(icon_width + icon_spacing, 0.0);
let mut min_size = Vec2::splat(spacing.interact_size.y);
min_size.y = min_size.y.at_least(icon_width);
let wrap_width = ui.available_width() - total_extra.x;
let text = text.into_galley(ui, None, wrap_width, TextStyle::Button);
// In order to center the checkbox based on min_size we set the icon height to at least min_size.y
let mut icon_size = Vec2::splat(icon_width);
icon_size.y = icon_size.y.at_least(min_size.y);
let rect_id = Id::new("egui::radio_button");
atoms.push_left(Atom::custom(rect_id, icon_size));
let mut desired_size = total_extra + text.size();
desired_size = desired_size.at_least(spacing.interact_size);
let text = atoms.text().map(String::from);
(Some(text), desired_size)
};
let mut prepared = AtomLayout::new(atoms)
.sense(Sense::click())
.min_size(min_size)
.allocate(ui);
desired_size = desired_size.at_least(Vec2::splat(spacing.interact_size.y));
desired_size.y = desired_size.y.max(icon_width);
let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click());
response.widget_info(|| {
prepared.response.widget_info(|| {
WidgetInfo::selected(
WidgetType::RadioButton,
ui.is_enabled(),
checked,
galley.as_ref().map_or("", |x| x.text()),
text.as_deref().unwrap_or(""),
)
});
if ui.is_rect_visible(rect) {
if ui.is_rect_visible(prepared.response.rect) {
// let visuals = ui.style().interact_selectable(&response, checked); // too colorful
let visuals = ui.style().interact(&response);
let visuals = *ui.style().interact(&prepared.response);
let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect);
prepared.fallback_text_color = visuals.text_color();
let response = prepared.paint(ui);
let painter = ui.painter();
if let Some(rect) = response.rect(rect_id) {
let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect);
painter.add(epaint::CircleShape {
center: big_icon_rect.center(),
radius: big_icon_rect.width() / 2.0 + visuals.expansion,
fill: visuals.bg_fill,
stroke: visuals.bg_stroke,
});
let painter = ui.painter();
if checked {
painter.add(epaint::CircleShape {
center: small_icon_rect.center(),
radius: small_icon_rect.width() / 3.0,
fill: visuals.fg_stroke.color, // Intentional to use stroke and not fill
// fill: ui.visuals().selection.stroke.color, // too much color
stroke: Default::default(),
center: big_icon_rect.center(),
radius: big_icon_rect.width() / 2.0 + visuals.expansion,
fill: visuals.bg_fill,
stroke: visuals.bg_stroke,
});
}
if let Some(galley) = galley {
let text_pos = pos2(
rect.min.x + icon_width + icon_spacing,
rect.center().y - 0.5 * galley.size().y,
);
ui.painter().galley(text_pos, galley, visuals.text_color());
if checked {
painter.add(epaint::CircleShape {
center: small_icon_rect.center(),
radius: small_icon_rect.width() / 3.0,
fill: visuals.fg_stroke.color, // Intentional to use stroke and not fill
// fill: ui.visuals().selection.stroke.color, // too much color
stroke: Default::default(),
});
}
}
response.response
} else {
prepared.response
}
response
}
}

View File

@@ -1,4 +1,6 @@
use crate::{NumExt, Response, Sense, TextStyle, Ui, Widget, WidgetInfo, WidgetText, WidgetType};
use crate::{
NumExt as _, Response, Sense, TextStyle, Ui, Widget, WidgetInfo, WidgetText, WidgetType,
};
/// One out of several alternatives, either selected or not.
/// Will mark selected items with a different background color.

View File

@@ -4,8 +4,8 @@ use std::ops::RangeInclusive;
use crate::{
emath, epaint, lerp, pos2, remap, remap_clamp, style, style::HandleShape, vec2, Color32,
DragValue, EventFilter, Key, Label, NumExt, Pos2, Rangef, Rect, Response, Sense, TextStyle,
TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, MINUS_CHAR_STR,
DragValue, EventFilter, Key, Label, NumExt as _, Pos2, Rangef, Rect, Response, Sense,
TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, MINUS_CHAR_STR,
};
use super::drag_value::clamp_value_to_range;
@@ -117,6 +117,7 @@ pub struct Slider<'a> {
custom_parser: Option<NumParser<'a>>,
trailing_fill: Option<bool>,
handle_shape: Option<HandleShape>,
update_while_editing: bool,
}
impl<'a> Slider<'a> {
@@ -167,6 +168,7 @@ impl<'a> Slider<'a> {
custom_parser: None,
trailing_fill: None,
handle_shape: None,
update_while_editing: true,
}
}
@@ -641,6 +643,16 @@ impl<'a> Slider<'a> {
let normalized = normalized_from_value(value, self.range(), &self.spec);
lerp(position_range, normalized as f32)
}
/// Update the value on each key press when text-editing the value.
///
/// Default: `true`.
/// If `false`, the value will only be updated when user presses enter or deselects the value.
#[inline]
pub fn update_while_editing(mut self, update: bool) -> Self {
self.update_while_editing = update;
self
}
}
impl Slider<'_> {
@@ -900,7 +912,8 @@ impl Slider<'_> {
.min_decimals(self.min_decimals)
.max_decimals_opt(self.max_decimals)
.suffix(self.suffix.clone())
.prefix(self.prefix.clone());
.prefix(self.prefix.clone())
.update_while_editing(self.update_while_editing);
match self.clamping {
SliderClamping::Never => {}

View File

@@ -43,7 +43,7 @@ impl Spinner {
.color
.unwrap_or_else(|| ui.visuals().strong_text_color());
let radius = (rect.height() / 2.0) - 2.0;
let n_points = 20;
let n_points = (radius.round() as u32).clamp(8, 128);
let time = ui.input(|i| i.time);
let start_angle = time * std::f64::consts::TAU;
let end_angle = start_angle + 240f64.to_radians() * time.sin();

View File

@@ -13,8 +13,8 @@ use crate::{
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, Response, Sense, Shape, TextBuffer,
TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetWithState,
ImeEvent, Key, KeyboardShortcut, Margin, Modifiers, NumExt as _, Response, Sense, Shape,
TextBuffer, TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetWithState,
};
use super::{TextEditOutput, TextEditState};
@@ -617,7 +617,7 @@ impl TextEdit<'_> {
}
let mut cursor_range = None;
let prev_cursor_range = state.cursor.char_range();
let prev_cursor_range = state.cursor.range(&galley);
if interactive && ui.memory(|mem| mem.has_focus(id)) {
ui.memory_mut(|mem| mem.set_focus_lock_filter(id, event_filter));
@@ -720,7 +720,7 @@ impl TextEdit<'_> {
let has_focus = ui.memory(|mem| mem.has_focus(id));
if has_focus {
if let Some(cursor_range) = state.cursor.char_range() {
if let Some(cursor_range) = state.cursor.range(&galley) {
// Add text selection rectangles to the galley:
paint_text_selection(&mut galley, ui.visuals(), &cursor_range, None);
}
@@ -742,7 +742,7 @@ impl TextEdit<'_> {
painter.galley(galley_pos, galley.clone(), text_color);
if has_focus {
if let Some(cursor_range) = state.cursor.char_range() {
if let Some(cursor_range) = state.cursor.range(&galley) {
let primary_cursor_rect =
cursor_rect(&galley, &cursor_range.primary, row_height)
.translate(galley_pos.to_vec2());
@@ -863,9 +863,11 @@ impl TextEdit<'_> {
fn mask_if_password(is_password: bool, text: &str) -> String {
fn mask_password(text: &str) -> String {
std::iter::repeat(epaint::text::PASSWORD_REPLACEMENT_CHAR)
.take(text.chars().count())
.collect::<String>()
std::iter::repeat_n(
epaint::text::PASSWORD_REPLACEMENT_CHAR,
text.chars().count(),
)
.collect::<String>()
}
if is_password {
@@ -878,7 +880,7 @@ fn mask_if_password(is_password: bool, text: &str) -> String {
// ----------------------------------------------------------------------------
/// Check for (keyboard) events to edit the cursor and/or text.
#[allow(clippy::too_many_arguments)]
#[expect(clippy::too_many_arguments)]
fn events(
ui: &crate::Ui,
state: &mut TextEditState,
@@ -896,7 +898,7 @@ fn events(
) -> (bool, CCursorRange) {
let os = ui.ctx().os();
let mut cursor_range = state.cursor.char_range().unwrap_or(default_cursor_range);
let mut cursor_range = state.cursor.range(galley).unwrap_or(default_cursor_range);
// We feed state to the undoer both before and after handling input
// so that the undoer creates automatic saves even when there are no events for a while.

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