1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-29 21:00:03 -04:00

Merge branch 'master' into axis-labels

This commit is contained in:
Emil Ernerfeldt
2023-08-14 13:23:26 +02:00
156 changed files with 1778 additions and 1026 deletions

View File

@@ -2,7 +2,8 @@
All notable changes to the `ecolor` crate will be noted in this file.
## Unreleased
This file is updated upon each release.
Changes since the last release can be found by running the `scripts/generate_changelog.py` script.
## 0.22.0 - 2023-05-23

View File

@@ -7,7 +7,7 @@ authors = [
]
description = "Color structs and color conversion utilities"
edition = "2021"
rust-version = "1.65"
rust-version = "1.67"
homepage = "https://github.com/emilk/egui"
license = "MIT OR Apache-2.0"
readme = "README.md"

View File

@@ -3,8 +3,8 @@ All notable changes to the `eframe` crate.
NOTE: [`egui-winit`](../egui-winit/CHANGELOG.md), [`egui_glium`](../egui_glium/CHANGELOG.md), [`egui_glow`](../egui_glow/CHANGELOG.md),and [`egui-wgpu`](../egui-wgpu/CHANGELOG.md) have their own changelogs!
## Unreleased
This file is updated upon each release.
Changes since the last release can be found by running the `scripts/generate_changelog.py` script.
## 0.22.0 - 2023-05-23
@@ -29,7 +29,7 @@ NOTE: [`egui-winit`](../egui-winit/CHANGELOG.md), [`egui_glium`](../egui_glium/C
* Read and request window focus [#2900](https://github.com/emilk/egui/pull/2900) (thanks [@TicClick](https://github.com/TicClick)!)
* Set app icon on Mac and Windows [#2940](https://github.com/emilk/egui/pull/2940)
* Set a default icon for all eframe apps: a white `e` on black background [#2996](https://github.com/emilk/egui/pull/2996)
* Add `NativeOptions::app_id` for the persistance location [#3014](https://github.com/emilk/egui/pull/3014) and for Wayland [#3007](https://github.com/emilk/egui/pull/3007) (thanks [@thomaskrause](https://github.com/thomaskrause)!)
* Add `NativeOptions::app_id` for the persistence location [#3014](https://github.com/emilk/egui/pull/3014) and for Wayland [#3007](https://github.com/emilk/egui/pull/3007) (thanks [@thomaskrause](https://github.com/thomaskrause)!)
* capture a screenshot using `Frame::request_screenshot` [870264b](https://github.com/emilk/egui/commit/870264b00577a95d3fd9bdf36efaf87fd351de62)

View File

@@ -4,7 +4,7 @@ version = "0.22.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "egui framework - write GUI apps that compiles to web and/or natively"
edition = "2021"
rust-version = "1.65"
rust-version = "1.67"
homepage = "https://github.com/emilk/egui/tree/master/crates/eframe"
license = "MIT OR Apache-2.0"
readme = "README.md"
@@ -27,7 +27,14 @@ targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"]
[features]
default = ["accesskit", "default_fonts", "glow"]
default = [
"accesskit",
"default_fonts",
"glow",
"wayland",
"winit/default",
"x11",
]
## Enable platform accessibility API implementations through [AccessKit](https://accesskit.dev/).
accesskit = ["egui/accesskit", "egui-winit/accesskit"]
@@ -42,6 +49,9 @@ glow = ["dep:glow", "dep:egui_glow", "dep:glutin", "dep:glutin-winit"]
## Enables wayland support and fixes clipboard issue.
wayland = ["egui-winit/wayland"]
## Enables compiling for x11.
x11 = ["egui-winit/x11"]
## Enable saving app state to disk.
persistence = [
"directories-next",
@@ -96,6 +106,7 @@ egui_glow = { version = "0.22.0", path = "../egui_glow", optional = true, defaul
glow = { version = "0.12", optional = true }
ron = { version = "0.8", optional = true, features = ["integer128"] }
serde = { version = "1", optional = true, features = ["derive"] }
static_assertions = "1.1.0"
# -------------------------------------------
# native:
@@ -108,7 +119,7 @@ image = { version = "0.24", default-features = false, features = [
"png",
] } # Needed for app icon
raw-window-handle = { version = "0.5.0" }
winit = "0.28.1"
winit = { version = "0.28.1", default-features = false }
# optional native:
directories-next = { version = "2", optional = true }
@@ -121,8 +132,8 @@ pollster = { version = "0.3", optional = true } # needed for wgpu
# this can be done at the same time we expose x11/wayland features of winit crate.
glutin = { version = "0.30", optional = true }
glutin-winit = { version = "0.3.0", optional = true }
puffin = { version = "0.15", optional = true }
wgpu = { version = "0.16.0", optional = true }
puffin = { version = "0.16", optional = true }
wgpu = { workspace = true, optional = true }
# mac:
[target.'cfg(any(target_os = "macos"))'.dependencies]
@@ -139,7 +150,7 @@ winapi = "0.3.9"
bytemuck = "1.7"
js-sys = "0.3"
percent-encoding = "2.1"
wasm-bindgen = "0.2.86"
wasm-bindgen = "0.2.87"
wasm-bindgen-futures = "0.4"
web-sys = { version = "0.3.58", features = [
"BinaryType",
@@ -189,4 +200,4 @@ web-sys = { version = "0.3.58", features = [
egui-wgpu = { version = "0.22.0", path = "../egui-wgpu", optional = true } # if wgpu is used, use it without (!) winit
raw-window-handle = { version = "0.5.2", optional = true }
tts = { version = "0.25", optional = true, default-features = false }
wgpu = { version = "0.16.0", optional = true }
wgpu = { workspace = true, optional = true }

View File

@@ -19,6 +19,13 @@ use std::any::Any;
#[cfg(any(feature = "glow", feature = "wgpu"))]
pub use crate::native::run::UserEvent;
#[cfg(not(target_arch = "wasm32"))]
use raw_window_handle::{
HasRawDisplayHandle, HasRawWindowHandle, RawDisplayHandle, RawWindowHandle,
};
#[cfg(not(target_arch = "wasm32"))]
use static_assertions::assert_not_impl_any;
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu"))]
pub use winit::event_loop::EventLoopBuilder;
@@ -64,6 +71,34 @@ pub struct CreationContext<'s> {
/// Can be used to manage GPU resources for custom rendering with WGPU using [`egui::PaintCallback`]s.
#[cfg(feature = "wgpu")]
pub wgpu_render_state: Option<egui_wgpu::RenderState>,
/// Raw platform window handle
#[cfg(not(target_arch = "wasm32"))]
pub(crate) raw_window_handle: RawWindowHandle,
/// Raw platform display handle for window
#[cfg(not(target_arch = "wasm32"))]
pub(crate) raw_display_handle: RawDisplayHandle,
}
// Implementing `Clone` would violate the guarantees of `HasRawWindowHandle` and `HasRawDisplayHandle`.
#[cfg(not(target_arch = "wasm32"))]
assert_not_impl_any!(CreationContext<'_>: Clone);
#[allow(unsafe_code)]
#[cfg(not(target_arch = "wasm32"))]
unsafe impl HasRawWindowHandle for CreationContext<'_> {
fn raw_window_handle(&self) -> RawWindowHandle {
self.raw_window_handle
}
}
#[allow(unsafe_code)]
#[cfg(not(target_arch = "wasm32"))]
unsafe impl HasRawDisplayHandle for CreationContext<'_> {
fn raw_display_handle(&self) -> RawDisplayHandle {
self.raw_display_handle
}
}
// ----------------------------------------------------------------------------
@@ -396,7 +431,7 @@ pub struct NativeOptions {
/// will be used instead.
///
/// ### On Wayland
/// On Wauland this sets the Application ID for the window.
/// On Wayland this sets the Application ID for the window.
///
/// The application ID is used in several places of the compositor, e.g. for
/// grouping windows of the same application. It is also important for
@@ -695,6 +730,34 @@ pub struct Frame {
/// such that it can be retrieved during [`App::post_rendering`] with [`Frame::screenshot`]
#[cfg(not(target_arch = "wasm32"))]
pub(crate) screenshot: std::cell::Cell<Option<egui::ColorImage>>,
/// Raw platform window handle
#[cfg(not(target_arch = "wasm32"))]
pub(crate) raw_window_handle: RawWindowHandle,
/// Raw platform display handle for window
#[cfg(not(target_arch = "wasm32"))]
pub(crate) raw_display_handle: RawDisplayHandle,
}
// Implementing `Clone` would violate the guarantees of `HasRawWindowHandle` and `HasRawDisplayHandle`.
#[cfg(not(target_arch = "wasm32"))]
assert_not_impl_any!(Frame: Clone);
#[allow(unsafe_code)]
#[cfg(not(target_arch = "wasm32"))]
unsafe impl HasRawWindowHandle for Frame {
fn raw_window_handle(&self) -> RawWindowHandle {
self.raw_window_handle
}
}
#[allow(unsafe_code)]
#[cfg(not(target_arch = "wasm32"))]
unsafe impl HasRawDisplayHandle for Frame {
fn raw_display_handle(&self) -> RawDisplayHandle {
self.raw_display_handle
}
}
impl Frame {
@@ -736,6 +799,7 @@ impl Frame {
/// * Called in [`App::update`]
/// * [`Frame::request_screenshot`] wasn't called on this frame during [`App::update`]
/// * The rendering backend doesn't support this feature (yet). Currently implemented for wgpu and glow, but not with wasm as target.
/// * Wgpu's GL target is active (not yet supported)
/// * Retrieving the data was unsuccessful in some way.
///
/// See also [`egui::ColorImage::region`]

View File

@@ -7,7 +7,7 @@
//! 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
//! call [`crate::run_native`] from your `main.rs`, and/or call `eframe::start_web` from your `lib.rs`.
//! call [`crate::run_native`] from your `main.rs`, and/or use `eframe::WebRunner` from your `lib.rs`.
//!
//! ## Usage, native:
//! ``` no_run
@@ -272,6 +272,7 @@ pub fn run_simple_native(
struct SimpleApp<U> {
update_fun: U,
}
impl<U: FnMut(&egui::Context, &mut Frame)> App for SimpleApp<U> {
fn update(&mut self, ctx: &egui::Context, frame: &mut Frame) {
(self.update_fun)(ctx, frame);

View File

@@ -3,6 +3,8 @@ use winit::event_loop::EventLoopWindowTarget;
#[cfg(target_os = "macos")]
use winit::platform::macos::WindowBuilderExtMacOS as _;
use raw_window_handle::{HasRawDisplayHandle as _, HasRawWindowHandle as _};
#[cfg(feature = "accesskit")]
use egui::accesskit;
use egui::NumExt as _;
@@ -119,9 +121,12 @@ pub fn window_builder<E>(
}
#[cfg(all(feature = "wayland", target_os = "linux"))]
if let Some(app_id) = &native_options.app_id {
{
use winit::platform::wayland::WindowBuilderExtWayland as _;
window_builder = window_builder.with_name(app_id, "");
match &native_options.app_id {
Some(app_id) => window_builder = window_builder.with_name(app_id, ""),
None => window_builder = window_builder.with_name(title, ""),
}
}
if let Some(min_size) = *min_window_size {
@@ -135,10 +140,11 @@ pub fn window_builder<E>(
let inner_size_points = if let Some(mut window_settings) = window_settings {
// Restore pos/size from previous session
window_settings.clamp_to_sane_values(largest_monitor_point_size(event_loop));
#[cfg(windows)]
window_settings.clamp_window_to_sane_position(event_loop);
window_builder = window_settings.initialize_window(window_builder);
window_settings.clamp_size_to_sane_values(largest_monitor_point_size(event_loop));
window_settings.clamp_position_to_monitors(event_loop);
window_builder = window_settings.initialize_window_builder(window_builder);
window_settings.inner_size_points()
} else {
if let Some(pos) = *initial_window_pos {
@@ -168,12 +174,14 @@ pub fn window_builder<E>(
}
}
}
window_builder
}
pub fn apply_native_options_to_window(
window: &winit::window::Window,
native_options: &crate::NativeOptions,
window_settings: Option<WindowSettings>,
) {
use winit::window::WindowLevel;
window.set_window_level(if native_options.always_on_top {
@@ -181,6 +189,10 @@ pub fn apply_native_options_to_window(
} else {
WindowLevel::Normal
});
if let Some(window_settings) = window_settings {
window_settings.initialize_window(window);
}
}
fn largest_monitor_point_size<E>(event_loop: &EventLoopWindowTarget<E>) -> egui::Vec2 {
@@ -330,8 +342,10 @@ pub struct EpiIntegration {
pub egui_ctx: egui::Context,
pending_full_output: egui::FullOutput,
egui_winit: egui_winit::State,
/// When set, it is time to close the native window.
close: bool,
can_drag_window: bool,
window_state: WindowState,
follow_system_theme: bool,
@@ -380,6 +394,8 @@ impl EpiIntegration {
#[cfg(feature = "wgpu")]
wgpu_render_state,
screenshot: std::cell::Cell::new(None),
raw_display_handle: window.raw_display_handle(),
raw_window_handle: window.raw_window_handle(),
};
let mut egui_winit = egui_winit::State::new(event_loop);
@@ -558,24 +574,26 @@ impl EpiIntegration {
pub fn maybe_autosave(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) {
let now = std::time::Instant::now();
if now - self.last_auto_save > app.auto_save_interval() {
self.save(app, window);
self.save(app, Some(window));
self.last_auto_save = now;
}
}
#[allow(clippy::unused_self)]
pub fn save(&mut self, _app: &mut dyn epi::App, _window: &winit::window::Window) {
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() {
crate::profile_function!();
if _app.persist_native_window() {
crate::profile_scope!("native_window");
epi::set_value(
storage,
STORAGE_WINDOW_KEY,
&WindowSettings::from_display(_window),
);
if let Some(window) = _window {
if _app.persist_native_window() {
crate::profile_scope!("native_window");
epi::set_value(
storage,
STORAGE_WINDOW_KEY,
&WindowSettings::from_display(window),
);
}
}
if _app.persist_egui_memory() {
crate::profile_scope!("egui_memory");

View File

@@ -80,14 +80,45 @@ impl crate::Storage for FileStorage {
join_handle.join().ok();
}
let join_handle = std::thread::spawn(move || {
let file = std::fs::File::create(&file_path).unwrap();
let config = Default::default();
ron::ser::to_writer_pretty(file, &kv, config).unwrap();
log::trace!("Persisted to {:?}", file_path);
});
match std::thread::Builder::new()
.name("eframe_persist".to_owned())
.spawn(move || {
save_to_disk(&file_path, &kv);
}) {
Ok(join_handle) => {
self.last_save_join_handle = Some(join_handle);
}
Err(err) => {
log::warn!("Failed to spawn thread to save app state: {err}");
}
}
}
}
}
self.last_save_join_handle = Some(join_handle);
fn save_to_disk(file_path: &PathBuf, kv: &HashMap<String, String>) {
crate::profile_function!();
if let Some(parent_dir) = file_path.parent() {
if !parent_dir.exists() {
if let Err(err) = std::fs::create_dir_all(parent_dir) {
log::warn!("Failed to create directory {parent_dir:?}: {err}");
}
}
}
match std::fs::File::create(file_path) {
Ok(file) => {
let config = Default::default();
if let Err(err) = ron::ser::to_writer_pretty(file, &kv, config) {
log::warn!("Failed to serialize app state: {err}");
} else {
log::trace!("Persisted to {:?}", file_path);
}
}
Err(err) => {
log::warn!("Failed to create file {file_path:?}: {err}");
}
}
}

View File

@@ -3,6 +3,7 @@
use std::time::Instant;
use raw_window_handle::{HasRawDisplayHandle as _, HasRawWindowHandle as _};
use winit::event_loop::{
ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget,
};
@@ -21,6 +22,7 @@ use super::epi_integration::{self, EpiIntegration};
pub enum UserEvent {
RequestRepaint {
when: Instant,
/// What the frame number was when the repaint was _requested_.
frame_nr: u64,
},
@@ -143,11 +145,13 @@ fn run_and_return(
// Platform-dependent event handlers to workaround a winit bug
// See: https://github.com/rust-windowing/winit/issues/987
// See: https://github.com/rust-windowing/winit/issues/1619
winit::event::Event::RedrawEventsCleared if cfg!(windows) => {
#[cfg(target_os = "windows")]
winit::event::Event::RedrawEventsCleared => {
next_repaint_time = extremely_far_future();
winit_app.run_ui_and_paint()
}
winit::event::Event::RedrawRequested(_) if !cfg!(windows) => {
#[cfg(not(target_os = "windows"))]
winit::event::Event::RedrawRequested(_) => {
next_repaint_time = extremely_far_future();
winit_app.run_ui_and_paint()
}
@@ -192,7 +196,7 @@ fn run_and_return(
EventResult::Wait => {}
EventResult::RepaintNow => {
log::trace!("Repaint caused by winit::Event: {:?}", event);
if cfg!(windows) {
if cfg!(target_os = "windows") {
// Fix flickering on Windows, see https://github.com/emilk/egui/pull/2280
next_repaint_time = extremely_far_future();
winit_app.run_ui_and_paint();
@@ -238,9 +242,15 @@ fn run_and_return(
// On Windows this clears out events so that we can later create another window.
// See https://github.com/emilk/egui/pull/1889 for details.
event_loop.run_return(|_, _, control_flow| {
control_flow.set_exit();
});
//
// Note that this approach may cause issues on macOS (emilk/egui#2768); therefore,
// we only apply this approach on Windows to minimize the affect.
#[cfg(target_os = "windows")]
{
event_loop.run_return(|_, _, control_flow| {
control_flow.set_exit();
});
}
returned_result
}
@@ -260,11 +270,11 @@ fn run_and_exit(event_loop: EventLoop<UserEvent>, mut winit_app: impl WinitApp +
// Platform-dependent event handlers to workaround a winit bug
// See: https://github.com/rust-windowing/winit/issues/987
// See: https://github.com/rust-windowing/winit/issues/1619
winit::event::Event::RedrawEventsCleared if cfg!(windows) => {
winit::event::Event::RedrawEventsCleared if cfg!(target_os = "windows") => {
next_repaint_time = extremely_far_future();
winit_app.run_ui_and_paint()
}
winit::event::Event::RedrawRequested(_) if !cfg!(windows) => {
winit::event::Event::RedrawRequested(_) if !cfg!(target_os = "windows") => {
next_repaint_time = extremely_far_future();
winit_app.run_ui_and_paint()
}
@@ -292,7 +302,7 @@ fn run_and_exit(event_loop: EventLoop<UserEvent>, mut winit_app: impl WinitApp +
match event_result {
EventResult::Wait => {}
EventResult::RepaintNow => {
if cfg!(windows) {
if cfg!(target_os = "windows") {
// Fix flickering on Windows, see https://github.com/emilk/egui/pull/2280
next_repaint_time = extremely_far_future();
winit_app.run_ui_and_paint();
@@ -339,7 +349,6 @@ mod glow_integration {
prelude::{GlDisplay, NotCurrentGlContextSurfaceAccessor, PossiblyCurrentGlContext},
surface::GlSurface,
};
use raw_window_handle::HasRawWindowHandle;
use super::*;
@@ -666,7 +675,11 @@ mod glow_integration {
glutin_window_context.on_resume(event_loop)?;
if let Some(window) = &glutin_window_context.window {
epi_integration::apply_native_options_to_window(window, native_options);
epi_integration::apply_native_options_to_window(
window,
native_options,
window_settings,
);
}
let gl = unsafe {
@@ -699,7 +712,7 @@ mod glow_integration {
let painter =
egui_glow::Painter::new(gl.clone(), "", self.native_options.shader_version)
.unwrap_or_else(|error| panic!("some OpenGL error occurred {}\n", error));
.unwrap_or_else(|err| panic!("An OpenGL error occurred: {err}\n"));
let system_theme = system_theme(gl_window.window(), &self.native_options);
let mut integration = epi_integration::EpiIntegration::new(
@@ -750,6 +763,8 @@ mod glow_integration {
gl: Some(gl.clone()),
#[cfg(feature = "wgpu")]
wgpu_render_state: None,
raw_display_handle: gl_window.window().raw_display_handle(),
raw_window_handle: gl_window.window().raw_window_handle(),
});
if app.warm_up_enabled() {
@@ -791,7 +806,7 @@ mod glow_integration {
if let Some(mut running) = self.running.take() {
running
.integration
.save(running.app.as_mut(), running.gl_window.window());
.save(running.app.as_mut(), running.gl_window.window.as_ref());
running.app.on_exit(Some(&running.gl));
running.painter.destroy();
}
@@ -799,6 +814,10 @@ mod glow_integration {
fn run_ui_and_paint(&mut self) -> EventResult {
if let Some(running) = &mut self.running {
if running.gl_window.window.is_none() {
return EventResult::Wait;
}
#[cfg(feature = "puffin")]
puffin::GlobalProfiler::lock().new_frame();
crate::profile_scope!("frame");
@@ -902,12 +921,9 @@ mod glow_integration {
integration.maybe_autosave(app.as_mut(), window);
if !self.is_focused {
// On Mac, a minimized Window uses up all CPU: https://github.com/emilk/egui/issues/325
// We can't know if we are minimized: https://github.com/rust-windowing/winit/issues/208
// But we know if we are focused (in foreground). When minimized, we are not focused.
// However, a user may want an egui with an animation in the background,
// so we still need to repaint quite fast.
if window.is_minimized() == Some(true) {
// On Mac, a minimized Window uses up all CPU:
// https://github.com/emilk/egui/issues/325
crate::profile_scope!("bg_sleep");
std::thread::sleep(std::time::Duration::from_millis(10));
}
@@ -1116,7 +1132,11 @@ mod wgpu_integration {
let window_builder =
epi_integration::window_builder(event_loop, title, native_options, window_settings);
let window = window_builder.build(event_loop)?;
epi_integration::apply_native_options_to_window(&window, native_options);
epi_integration::apply_native_options_to_window(
&window,
native_options,
window_settings,
);
Ok(window)
}
@@ -1209,6 +1229,8 @@ mod wgpu_integration {
#[cfg(feature = "glow")]
gl: None,
wgpu_render_state,
raw_display_handle: window.raw_display_handle(),
raw_window_handle: window.raw_window_handle(),
});
if app.warm_up_enabled() {
@@ -1247,9 +1269,9 @@ mod wgpu_integration {
fn save_and_destroy(&mut self) {
if let Some(mut running) = self.running.take() {
if let Some(window) = &self.window {
running.integration.save(running.app.as_mut(), window);
}
running
.integration
.save(running.app.as_mut(), self.window.as_ref());
#[cfg(feature = "glow")]
running.app.on_exit(None);
@@ -1321,12 +1343,9 @@ mod wgpu_integration {
integration.maybe_autosave(app.as_mut(), window);
if !self.is_focused {
// On Mac, a minimized Window uses up all CPU: https://github.com/emilk/egui/issues/325
// We can't know if we are minimized: https://github.com/rust-windowing/winit/issues/208
// But we know if we are focused (in foreground). When minimized, we are not focused.
// However, a user may want an egui with an animation in the background,
// so we still need to repaint quite fast.
if window.is_minimized() == Some(true) {
// On Mac, a minimized Window uses up all CPU:
// https://github.com/emilk/egui/issues/325
crate::profile_scope!("bg_sleep");
std::thread::sleep(std::time::Duration::from_millis(10));
}

View File

@@ -68,7 +68,7 @@ pub fn push_touches(runner: &mut AppRunner, phase: egui::TouchPhase, event: &web
id: egui::TouchId::from(touch.identifier()),
phase,
pos: pos_from_touch(canvas_origin, &touch),
force: touch.force(),
force: Some(touch.force()),
});
}
}

View File

@@ -104,7 +104,7 @@ pub fn canvas_element(canvas_id: &str) -> Option<web_sys::HtmlCanvasElement> {
pub fn canvas_element_or_die(canvas_id: &str) -> web_sys::HtmlCanvasElement {
canvas_element(canvas_id)
.unwrap_or_else(|| panic!("Failed to find canvas with id {:?}", canvas_id))
.unwrap_or_else(|| panic!("Failed to find canvas with id {canvas_id:?}"))
}
fn canvas_origin(canvas_id: &str) -> egui::Pos2 {

View File

@@ -104,8 +104,7 @@ pub fn install_text_agent(runner_ref: &WebRunner) -> Result<(), JsValue> {
runner_ref.add_event_listener(&input, "focusout", move |_event: web_sys::MouseEvent, _| {
// Delay 10 ms, and focus again.
let func = js_sys::Function::new_no_args(&format!(
"document.getElementById('{}').focus()",
AGENT_ID
"document.getElementById('{AGENT_ID}').focus()"
));
window
.set_timeout_with_callback_and_timeout_and_arguments_0(&func, 10)
@@ -221,8 +220,8 @@ pub fn move_text_cursor(cursor: Option<egui::Pos2>, canvas_id: &str) -> Option<(
let x = (x - canvas.offset_width() as f32 / 2.0)
.min(canvas.client_width() as f32 - bounding_rect.width() as f32);
style.set_property("position", "absolute").ok()?;
style.set_property("top", &format!("{}px", y)).ok()?;
style.set_property("left", &format!("{}px", x)).ok()
style.set_property("top", &format!("{y}px")).ok()?;
style.set_property("left", &format!("{x}px")).ok()
})
} else {
style.set_property("position", "absolute").ok()?;

View File

@@ -27,7 +27,7 @@ impl WebPainterGlow {
let gl = std::sync::Arc::new(gl);
let painter = egui_glow::Painter::new(gl, shader_prefix, None)
.map_err(|error| format!("Error starting glow painter: {}", error))?;
.map_err(|err| format!("Error starting glow painter: {err}"))?;
Ok(Self {
canvas,

View File

@@ -87,8 +87,7 @@ impl WebPainterWgpu {
} else {
// Workaround for https://github.com/gfx-rs/wgpu/issues/3710:
// Don't use `create_surface_from_canvas`, but `create_surface` instead!
let raw_window =
EguiWebWindow(egui::util::hash(&format!("egui on wgpu {canvas_id}")) as u32);
let raw_window = EguiWebWindow(egui::util::hash(("egui on wgpu", canvas_id)) as u32);
canvas.set_attribute("data-raw-handle", &raw_window.0.to_string());
#[allow(unsafe_code)]

View File

@@ -2,7 +2,8 @@
All notable changes to the `egui-wgpu` integration will be noted in this file.
## Unreleased
This file is updated upon each release.
Changes since the last release can be found by running the `scripts/generate_changelog.py` script.
## 0.22.0 - 2023-05-23

View File

@@ -8,7 +8,7 @@ authors = [
"Emil Ernerfeldt <emil.ernerfeldt@gmail.com>",
]
edition = "2021"
rust-version = "1.65"
rust-version = "1.67"
homepage = "https://github.com/emilk/egui/tree/master/crates/egui-wgpu"
license = "MIT OR Apache-2.0"
readme = "README.md"
@@ -44,14 +44,14 @@ bytemuck = "1.7"
log = { version = "0.4", features = ["std"] }
thiserror.workspace = true
type-map = "0.5.0"
wgpu = "0.16.0"
wgpu.workspace = true
#! ### Optional dependencies
## Enable this when generating docs.
document-features = { version = "0.2", optional = true }
winit = { version = "0.28", optional = true }
winit = { version = "0.28", default-features = false, optional = true }
# Native:
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
puffin = { version = "0.15", optional = true }
puffin = { version = "0.16", optional = true }

View File

@@ -560,7 +560,7 @@ impl Renderer {
} else {
// allocate a new texture
// Use same label for all resources associated with this texture id (no point in retyping the type)
let label_str = format!("egui_texid_{:?}", id);
let label_str = format!("egui_texid_{id:?}");
let label = Some(label_str.as_str());
let texture = device.create_texture(&wgpu::TextureDescriptor {
label,
@@ -904,8 +904,7 @@ fn create_sampler(
};
device.create_sampler(&wgpu::SamplerDescriptor {
label: Some(&format!(
"egui sampler (mag: {:?}, min {:?})",
mag_filter, min_filter
"egui sampler (mag: {mag_filter:?}, min {min_filter:?})"
)),
mag_filter,
min_filter,

View File

@@ -7,6 +7,7 @@ struct SurfaceState {
alpha_mode: wgpu::CompositeAlphaMode,
width: u32,
height: u32,
supports_screenshot: bool,
}
/// A texture and a buffer for reading the rendered frame back to the cpu.
@@ -136,10 +137,15 @@ impl Painter {
render_state: &RenderState,
present_mode: wgpu::PresentMode,
) {
let usage = if surface_state.supports_screenshot {
wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST
} else {
wgpu::TextureUsages::RENDER_ATTACHMENT
};
surface_state.surface.configure(
&render_state.device,
&wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST,
usage,
format: render_state.target_format,
width: surface_state.width,
height: surface_state.height,
@@ -218,12 +224,16 @@ impl Painter {
wgpu::CompositeAlphaMode::Auto
};
let supports_screenshot =
!matches!(render_state.adapter.get_info().backend, wgpu::Backend::Gl);
let size = window.inner_size();
self.surface_state = Some(SurfaceState {
surface,
width: size.width,
height: size.height,
alpha_mode,
supports_screenshot,
});
self.resize_and_generate_depth_texture_view_and_msaa_view(size.width, size.height);
}
@@ -269,7 +279,7 @@ impl Painter {
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
sample_count: self.msaa_samples,
dimension: wgpu::TextureDimension::D2,
format: depth_format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
@@ -485,6 +495,15 @@ impl Painter {
)
};
let capture = match (capture, surface_state.supports_screenshot) {
(false, _) => false,
(true, true) => true,
(true, false) => {
log::error!("The active render surface doesn't support taking screenshots.");
false
}
};
{
let renderer = render_state.renderer.read();
let frame_view = if capture {
@@ -566,7 +585,7 @@ impl Painter {
} else {
None
};
// Redraw egui
{
crate::profile_scope!("present");
output_frame.present();

View File

@@ -1,8 +1,8 @@
# Changelog for egui-winit
All notable changes to the `egui-winit` integration will be noted in this file.
## Unreleased
This file is updated upon each release.
Changes since the last release can be found by running the `scripts/generate_changelog.py` script.
## 0.22.0 - 2023-05-23

View File

@@ -4,7 +4,7 @@ version = "0.22.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "Bindings for using egui with winit"
edition = "2021"
rust-version = "1.65"
rust-version = "1.67"
homepage = "https://github.com/emilk/egui/tree/master/crates/egui-winit"
license = "MIT OR Apache-2.0"
readme = "README.md"
@@ -18,7 +18,7 @@ all-features = true
[features]
default = ["clipboard", "links", "wayland", "winit/default"]
default = ["clipboard", "links", "wayland", "winit/default", "x11"]
## Enable platform accessibility API implementations through [AccessKit](https://accesskit.dev/).
accesskit = ["accesskit_winit", "egui/accesskit"]
@@ -42,6 +42,9 @@ serde = ["egui/serde", "dep:serde"]
## Enables Wayland support.
wayland = ["winit/wayland"]
## Enables compiling for x11.
x11 = ["winit/x11"]
# Allow crates to choose an android-activity backend via Winit
# - It's important that most applications should not have to depend on android-activity directly, and can
# rely on Winit to pull in a suitable version (unlike most Rust crates, any version conflicts won't link)
@@ -68,7 +71,7 @@ accesskit_winit = { version = "0.14.0", optional = true }
## Enable this when generating docs.
document-features = { version = "0.2", optional = true }
puffin = { version = "0.15", optional = true }
puffin = { version = "0.16", optional = true }
serde = { version = "1.0", optional = true, features = ["derive"] }
webbrowser = { version = "0.8.3", optional = true }

View File

@@ -3,7 +3,7 @@ use raw_window_handle::HasRawDisplayHandle;
/// Handles interfacing with the OS clipboard.
///
/// If the "clipboard" feature is off, or we cannot connect to the OS clipboard,
/// then a fallback clipboard that just works works within the same app is used instead.
/// then a fallback clipboard that just works within the same app is used instead.
pub struct Clipboard {
#[cfg(all(feature = "arboard", not(target_os = "android")))]
arboard: Option<arboard::Clipboard>,

View File

@@ -307,6 +307,7 @@ impl State {
}
WindowEvent::KeyboardInput { input, .. } => {
self.on_keyboard_input(input);
// When pressing the Tab key, egui focuses the first focusable element, hence Tab always consumes.
let consumed = egui_ctx.wants_keyboard_input()
|| input.virtual_keycode == Some(winit::event::VirtualKeyCode::Tab);
EventResponse {
@@ -440,7 +441,7 @@ impl State {
id: egui::TouchId(0),
phase: egui::TouchPhase::Start,
pos,
force: 0.0,
force: None,
});
} else {
self.any_pointer_button_down = false;
@@ -452,7 +453,7 @@ impl State {
id: egui::TouchId(0),
phase: egui::TouchPhase::End,
pos,
force: 0.0,
force: None,
});
};
}
@@ -478,7 +479,7 @@ impl State {
id: egui::TouchId(0),
phase: egui::TouchPhase::Move,
pos: pos_in_points,
force: 0.0,
force: None,
});
}
} else {
@@ -504,13 +505,13 @@ impl State {
touch.location.y as f32 / self.pixels_per_point(),
),
force: match touch.force {
Some(winit::event::Force::Normalized(force)) => force as f32,
Some(winit::event::Force::Normalized(force)) => Some(force as f32),
Some(winit::event::Force::Calibrated {
force,
max_possible_force,
..
}) => (force / max_possible_force) as f32,
None => 0_f32,
}) => Some((force / max_possible_force) as f32),
None => None,
},
});
// If we're not yet translating a touch or we're translating this very

View File

@@ -1,11 +1,13 @@
/// Can be used to store native window settings (position and size).
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct WindowSettings {
/// Position of window in physical pixels. This is either
/// the inner or outer position depending on the platform.
/// See [`winit::window::WindowBuilder::with_position`] for details.
position: Option<egui::Pos2>,
/// Position of window content in physical pixels.
inner_position_pixels: Option<egui::Pos2>,
/// Position of window frame/titlebar in physical pixels.
outer_position_pixels: Option<egui::Pos2>,
fullscreen: bool,
@@ -16,22 +18,20 @@ pub struct WindowSettings {
impl WindowSettings {
pub fn from_display(window: &winit::window::Window) -> Self {
let inner_size_points = window.inner_size().to_logical::<f32>(window.scale_factor());
let position = if cfg!(macos) {
// MacOS uses inner position when positioning windows.
window
.inner_position()
.ok()
.map(|p| egui::pos2(p.x as f32, p.y as f32))
} else {
// Other platforms use the outer position.
window
.outer_position()
.ok()
.map(|p| egui::pos2(p.x as f32, p.y as f32))
};
let inner_position_pixels = window
.inner_position()
.ok()
.map(|p| egui::pos2(p.x as f32, p.y as f32));
let outer_position_pixels = window
.outer_position()
.ok()
.map(|p| egui::pos2(p.x as f32, p.y as f32));
Self {
position,
inner_position_pixels,
outer_position_pixels,
fullscreen: window.fullscreen().is_some(),
@@ -46,19 +46,21 @@ impl WindowSettings {
self.inner_size_points
}
pub fn initialize_window(
pub fn initialize_window_builder(
&self,
mut window: winit::window::WindowBuilder,
) -> winit::window::WindowBuilder {
// If the app last ran on two monitors and only one is now connected, then
// the given position is invalid.
// If this happens on Mac, the window is clamped into valid area.
// If this happens on Windows, the clamping behavior is managed by the function
// clamp_window_to_sane_position.
if let Some(pos) = self.position {
// `WindowBuilder::with_position` expects inner position in Macos, and outer position elsewhere
// See [`winit::window::WindowBuilder::with_position`] for details.
let pos_px = if cfg!(target_os = "macos") {
self.inner_position_pixels
} else {
self.outer_position_pixels
};
if let Some(pos_px) = pos_px {
window = window.with_position(winit::dpi::PhysicalPosition {
x: pos.x as f64,
y: pos.y as f64,
x: pos_px.x as f64,
y: pos_px.y as f64,
});
}
@@ -77,68 +79,103 @@ impl WindowSettings {
}
}
pub fn clamp_to_sane_values(&mut self, max_size: egui::Vec2) {
pub fn initialize_window(&self, window: &winit::window::Window) {
if cfg!(target_os = "macos") {
// Mac sometimes has problems restoring the window to secondary monitors
// using only `WindowBuilder::with_position`, so we need this extra step:
if let Some(pos) = self.outer_position_pixels {
window.set_outer_position(winit::dpi::PhysicalPosition { x: pos.x, y: pos.y });
}
}
}
pub fn clamp_size_to_sane_values(&mut self, largest_monitor_size_points: egui::Vec2) {
use egui::NumExt as _;
if let Some(size) = &mut self.inner_size_points {
// Prevent ridiculously small windows
// Prevent ridiculously small windows:
let min_size = egui::Vec2::splat(64.0);
*size = size.at_least(min_size);
*size = size.at_most(max_size);
// Make sure we don't try to create a window larger than the largest monitor
// because on Linux that can lead to a crash.
*size = size.at_most(largest_monitor_size_points);
}
}
pub fn clamp_window_to_sane_position<E>(
pub fn clamp_position_to_monitors<E>(
&mut self,
event_loop: &winit::event_loop::EventLoopWindowTarget<E>,
) {
if let (Some(position), Some(inner_size_points)) =
(&mut self.position, &self.inner_size_points)
{
let monitors = event_loop.available_monitors();
// default to primary monitor, in case the correct monitor was disconnected.
let mut active_monitor = if let Some(active_monitor) = event_loop
.primary_monitor()
.or_else(|| event_loop.available_monitors().next())
{
active_monitor
} else {
return; // no monitors 🤷
};
for monitor in monitors {
let monitor_x_range = (monitor.position().x - inner_size_points.x as i32)
..(monitor.position().x + monitor.size().width as i32);
let monitor_y_range = (monitor.position().y - inner_size_points.y as i32)
..(monitor.position().y + monitor.size().height as i32);
// If the app last ran on two monitors and only one is now connected, then
// the given position is invalid.
// If this happens on Mac, the window is clamped into valid area.
// If this happens on Windows, the window becomes invisible to the user 🤦‍♂️
// So on Windows we clamp the position to the monitor it is on.
if !cfg!(target_os = "windows") {
return;
}
if monitor_x_range.contains(&(position.x as i32))
&& monitor_y_range.contains(&(position.y as i32))
{
active_monitor = monitor;
}
}
let Some(inner_size_points) = self.inner_size_points else { return; };
let mut inner_size_pixels = *inner_size_points * (active_monitor.scale_factor() as f32);
// Add size of title bar. This is 32 px by default in Win 10/11.
if cfg!(target_os = "windows") {
inner_size_pixels +=
egui::Vec2::new(0.0, 32.0 * active_monitor.scale_factor() as f32);
}
let monitor_position = egui::Pos2::new(
active_monitor.position().x as f32,
active_monitor.position().y as f32,
);
let monitor_size = egui::Vec2::new(
active_monitor.size().width as f32,
active_monitor.size().height as f32,
);
// Window size cannot be negative or the subsequent `clamp` will panic.
let window_size = (monitor_size - inner_size_pixels).max(egui::Vec2::ZERO);
// To get the maximum position, we get the rightmost corner of the display, then
// subtract the size of the window to get the bottom right most value window.position
// can have.
*position = position.clamp(monitor_position, monitor_position + window_size);
if let Some(pos_px) = &mut self.inner_position_pixels {
clamp_pos_to_monitors(event_loop, inner_size_points, pos_px);
}
if let Some(pos_px) = &mut self.outer_position_pixels {
clamp_pos_to_monitors(event_loop, inner_size_points, pos_px);
}
}
}
fn clamp_pos_to_monitors<E>(
event_loop: &winit::event_loop::EventLoopWindowTarget<E>,
window_size_pts: egui::Vec2,
position_px: &mut egui::Pos2,
) {
let monitors = event_loop.available_monitors();
// default to primary monitor, in case the correct monitor was disconnected.
let mut active_monitor = if let Some(active_monitor) = event_loop
.primary_monitor()
.or_else(|| event_loop.available_monitors().next())
{
active_monitor
} else {
return; // no monitors 🤷
};
for monitor in monitors {
let window_size_px = window_size_pts * (monitor.scale_factor() as f32);
let monitor_x_range = (monitor.position().x - window_size_px.x as i32)
..(monitor.position().x + monitor.size().width as i32);
let monitor_y_range = (monitor.position().y - window_size_px.y as i32)
..(monitor.position().y + monitor.size().height as i32);
if monitor_x_range.contains(&(position_px.x as i32))
&& monitor_y_range.contains(&(position_px.y as i32))
{
active_monitor = monitor;
}
}
let mut window_size_px = window_size_pts * (active_monitor.scale_factor() as f32);
// Add size of title bar. This is 32 px by default in Win 10/11.
if cfg!(target_os = "windows") {
window_size_px += egui::Vec2::new(0.0, 32.0 * active_monitor.scale_factor() as f32);
}
let monitor_position = egui::Pos2::new(
active_monitor.position().x as f32,
active_monitor.position().y as f32,
);
let monitor_size_px = egui::Vec2::new(
active_monitor.size().width as f32,
active_monitor.size().height as f32,
);
// Window size cannot be negative or the subsequent `clamp` will panic.
let window_size = (monitor_size_px - window_size_px).max(egui::Vec2::ZERO);
// To get the maximum position, we get the rightmost corner of the display, then
// subtract the size of the window to get the bottom right most value window.position
// can have.
*position_px = position_px.clamp(monitor_position, monitor_position + window_size);
}

View File

@@ -4,7 +4,7 @@ version = "0.22.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "An easy-to-use immediate mode GUI that runs on both web and native"
edition = "2021"
rust-version = "1.65"
rust-version = "1.67"
homepage = "https://github.com/emilk/egui"
license = "MIT OR Apache-2.0"
readme = "../../README.md"

View File

@@ -426,7 +426,7 @@ impl Prepared {
temporarily_invisible: _,
} = self;
state.size = content_ui.min_rect().size();
state.size = content_ui.min_size();
ctx.memory_mut(|m| m.areas.set_state(layer_id, state));

View File

@@ -193,9 +193,7 @@ impl Frame {
let where_to_put_background = ui.painter().add(Shape::Noop);
let outer_rect_bounds = ui.available_rect_before_wrap();
let mut inner_rect = outer_rect_bounds;
inner_rect.min += self.outer_margin.left_top() + self.inner_margin.left_top();
inner_rect.max -= self.outer_margin.right_bottom() + self.inner_margin.right_bottom();
let mut inner_rect = (self.inner_margin + self.outer_margin).shrink_rect(outer_rect_bounds);
// Make sure we don't shrink to the negative:
inner_rect.max.x = inner_rect.max.x.max(inner_rect.min.x);
@@ -256,17 +254,13 @@ impl Frame {
impl Prepared {
fn paint_rect(&self) -> Rect {
let mut rect = self.content_ui.min_rect();
rect.min -= self.frame.inner_margin.left_top();
rect.max += self.frame.inner_margin.right_bottom();
rect
self.frame
.inner_margin
.expand_rect(self.content_ui.min_rect())
}
fn content_with_margin(&self) -> Rect {
let mut rect = self.content_ui.min_rect();
rect.min -= self.frame.inner_margin.left_top() + self.frame.outer_margin.left_top();
rect.max += self.frame.inner_margin.right_bottom() + self.frame.outer_margin.right_bottom();
rect
(self.frame.inner_margin + self.frame.outer_margin).expand_rect(self.content_ui.min_rect())
}
pub fn end(self, ui: &mut Ui) -> Response {

View File

@@ -15,8 +15,6 @@
//!
//! Add your [`Window`]:s after any top-level panels.
use std::ops::RangeInclusive;
use crate::*;
/// State regarding panels.
@@ -99,7 +97,7 @@ pub struct SidePanel {
resizable: bool,
show_separator_line: bool,
default_width: f32,
width_range: RangeInclusive<f32>,
width_range: Rangef,
}
impl SidePanel {
@@ -122,7 +120,7 @@ impl SidePanel {
resizable: true,
show_separator_line: true,
default_width: 200.0,
width_range: 96.0..=f32::INFINITY,
width_range: Rangef::new(96.0, f32::INFINITY),
}
}
@@ -153,26 +151,29 @@ impl SidePanel {
/// The initial wrapping width of the [`SidePanel`].
pub fn default_width(mut self, default_width: f32) -> Self {
self.default_width = default_width;
self.width_range = self.width_range.start().at_most(default_width)
..=self.width_range.end().at_least(default_width);
self.width_range = Rangef::new(
self.width_range.min.at_most(default_width),
self.width_range.max.at_least(default_width),
);
self
}
/// Minimum width of the panel.
pub fn min_width(mut self, min_width: f32) -> Self {
self.width_range = min_width..=self.width_range.end().at_least(min_width);
self.width_range = Rangef::new(min_width, self.width_range.max.at_least(min_width));
self
}
/// Maximum width of the panel.
pub fn max_width(mut self, max_width: f32) -> Self {
self.width_range = self.width_range.start().at_most(max_width)..=max_width;
self.width_range = Rangef::new(self.width_range.min.at_most(max_width), max_width);
self
}
/// The allowable width range for the panel.
pub fn width_range(mut self, width_range: RangeInclusive<f32>) -> Self {
self.default_width = clamp_to_range(self.default_width, width_range.clone());
pub fn width_range(mut self, width_range: impl Into<Rangef>) -> Self {
let width_range = width_range.into();
self.default_width = clamp_to_range(self.default_width, width_range);
self.width_range = width_range;
self
}
@@ -180,7 +181,7 @@ impl SidePanel {
/// Enforce this exact width.
pub fn exact_width(mut self, width: f32) -> Self {
self.default_width = width;
self.width_range = width..=width;
self.width_range = Rangef::point(width);
self
}
@@ -224,7 +225,7 @@ impl SidePanel {
if let Some(state) = PanelState::load(ui.ctx(), id) {
width = state.rect.width();
}
width = clamp_to_range(width, width_range.clone()).at_most(available_rect.width());
width = clamp_to_range(width, width_range).at_most(available_rect.width());
side.set_rect_width(&mut panel_rect, width);
ui.ctx().check_for_id_clash(id, panel_rect, "SidePanel");
}
@@ -241,7 +242,7 @@ impl SidePanel {
let resize_x = side.opposite().side_x(panel_rect);
let mouse_over_resize_line = we_are_on_top
&& panel_rect.y_range().contains(&pointer.y)
&& panel_rect.y_range().contains(pointer.y)
&& (resize_x - pointer.x).abs()
<= ui.style().interaction.resize_grab_radius_side;
@@ -253,8 +254,7 @@ impl SidePanel {
is_resizing = ui.memory(|mem| mem.is_being_dragged(resize_id));
if is_resizing {
let width = (pointer.x - side.side_x(panel_rect)).abs();
let width =
clamp_to_range(width, width_range.clone()).at_most(available_rect.width());
let width = clamp_to_range(width, width_range).at_most(available_rect.width());
side.set_rect_width(&mut panel_rect, width);
}
@@ -273,7 +273,7 @@ impl SidePanel {
let frame = frame.unwrap_or_else(|| Frame::side_top_panel(ui.style()));
let inner_response = frame.show(&mut panel_ui, |ui| {
ui.set_min_height(ui.max_rect().height()); // Make sure the frame fills the full height
ui.set_min_width(*width_range.start());
ui.set_min_width(width_range.min);
add_contents(ui)
});
@@ -544,7 +544,7 @@ pub struct TopBottomPanel {
resizable: bool,
show_separator_line: bool,
default_height: Option<f32>,
height_range: RangeInclusive<f32>,
height_range: Rangef,
}
impl TopBottomPanel {
@@ -567,7 +567,7 @@ impl TopBottomPanel {
resizable: false,
show_separator_line: true,
default_height: None,
height_range: 20.0..=f32::INFINITY,
height_range: Rangef::new(20.0, f32::INFINITY),
}
}
@@ -599,28 +599,31 @@ impl TopBottomPanel {
/// Defaults to [`style::Spacing::interact_size`].y.
pub fn default_height(mut self, default_height: f32) -> Self {
self.default_height = Some(default_height);
self.height_range = self.height_range.start().at_most(default_height)
..=self.height_range.end().at_least(default_height);
self.height_range = Rangef::new(
self.height_range.min.at_most(default_height),
self.height_range.max.at_least(default_height),
);
self
}
/// Minimum height of the panel.
pub fn min_height(mut self, min_height: f32) -> Self {
self.height_range = min_height..=self.height_range.end().at_least(min_height);
self.height_range = Rangef::new(min_height, self.height_range.max.at_least(min_height));
self
}
/// Maximum height of the panel.
pub fn max_height(mut self, max_height: f32) -> Self {
self.height_range = self.height_range.start().at_most(max_height)..=max_height;
self.height_range = Rangef::new(self.height_range.min.at_most(max_height), max_height);
self
}
/// The allowable height range for the panel.
pub fn height_range(mut self, height_range: RangeInclusive<f32>) -> Self {
pub fn height_range(mut self, height_range: impl Into<Rangef>) -> Self {
let height_range = height_range.into();
self.default_height = self
.default_height
.map(|default_height| clamp_to_range(default_height, height_range.clone()));
.map(|default_height| clamp_to_range(default_height, height_range));
self.height_range = height_range;
self
}
@@ -628,7 +631,7 @@ impl TopBottomPanel {
/// Enforce this exact height.
pub fn exact_height(mut self, height: f32) -> Self {
self.default_height = Some(height);
self.height_range = height..=height;
self.height_range = Rangef::point(height);
self
}
@@ -673,7 +676,7 @@ impl TopBottomPanel {
} else {
default_height.unwrap_or_else(|| ui.style().spacing.interact_size.y)
};
height = clamp_to_range(height, height_range.clone()).at_most(available_rect.height());
height = clamp_to_range(height, height_range).at_most(available_rect.height());
side.set_rect_height(&mut panel_rect, height);
ui.ctx()
.check_for_id_clash(id, panel_rect, "TopBottomPanel");
@@ -692,7 +695,7 @@ impl TopBottomPanel {
let resize_y = side.opposite().side_y(panel_rect);
let mouse_over_resize_line = we_are_on_top
&& panel_rect.x_range().contains(&pointer.x)
&& panel_rect.x_range().contains(pointer.x)
&& (resize_y - pointer.y).abs()
<= ui.style().interaction.resize_grab_radius_side;
@@ -704,8 +707,8 @@ impl TopBottomPanel {
is_resizing = ui.memory(|mem| mem.interaction.drag_id == Some(resize_id));
if is_resizing {
let height = (pointer.y - side.side_y(panel_rect)).abs();
let height = clamp_to_range(height, height_range.clone())
.at_most(available_rect.height());
let height =
clamp_to_range(height, height_range).at_most(available_rect.height());
side.set_rect_height(&mut panel_rect, height);
}
@@ -724,7 +727,7 @@ impl TopBottomPanel {
let frame = frame.unwrap_or_else(|| Frame::side_top_panel(ui.style()));
let inner_response = frame.show(&mut panel_ui, |ui| {
ui.set_min_width(ui.max_rect().width()); // Make the frame fill full width
ui.set_min_height(*height_range.start());
ui.set_min_height(height_range.min);
add_contents(ui)
});
@@ -1056,9 +1059,7 @@ impl CentralPanel {
}
}
fn clamp_to_range(x: f32, range: RangeInclusive<f32>) -> f32 {
x.clamp(
range.start().min(*range.end()),
range.start().max(*range.end()),
)
fn clamp_to_range(x: f32, range: Rangef) -> f32 {
let range = range.as_positive();
x.clamp(range.min, range.max)
}

View File

@@ -124,7 +124,10 @@ impl Resize {
}
/// Can you resize it with the mouse?
/// Note that a window can still auto-resize
///
/// Note that a window can still auto-resize.
///
/// Default is `true`.
pub fn resizable(mut self, resizable: bool) -> Self {
self.resizable = resizable;
self

View File

@@ -334,16 +334,22 @@ struct Prepared {
state: State,
has_bar: [bool; 2],
auto_shrink: [bool; 2],
/// How much horizontal and vertical space are used up by the
/// width of the vertical bar, and the height of the horizontal bar?
current_bar_use: Vec2,
scroll_bar_visibility: ScrollBarVisibility,
/// Where on the screen the content is (excludes scroll bars).
inner_rect: Rect,
content_ui: Ui,
/// Relative coordinates: the offset and size of the view of the inner UI.
/// `viewport.min == ZERO` means we scrolled to the top.
viewport: Rect,
scrolling_enabled: bool,
stick_to_end: [bool; 2],
}
@@ -459,7 +465,7 @@ impl ScrollArea {
content_clip_rect.max[d] = ui.clip_rect().max[d] - current_bar_use[d];
}
}
// Make sure we din't accidentally expand the clip rect
// Make sure we didn't accidentally expand the clip rect
content_clip_rect = content_clip_rect.intersect(ui.clip_rect());
content_ui.set_clip_rect(content_clip_rect);
}
@@ -640,8 +646,7 @@ impl Prepared {
let min = content_ui.min_rect().min[d];
let clip_rect = content_ui.clip_rect();
let visible_range = min..=min + clip_rect.size()[d];
let start = *scroll.start();
let end = *scroll.end();
let (start, end) = (scroll.min, scroll.max);
let clip_start = clip_rect.min[d];
let clip_end = clip_rect.max[d];
let mut spacing = ui.spacing().item_spacing[d];

View File

@@ -217,7 +217,10 @@ impl<'open> Window<'open> {
}
/// Can the user resize the window by dragging its edges?
///
/// Note that even if you set this to `false` the window may still auto-resize.
///
/// Default is `true`.
pub fn resizable(mut self, resizable: bool) -> Self {
self.resize = self.resize.resizable(resizable);
self

View File

@@ -566,7 +566,7 @@ impl Context {
}
let show_error = |widget_rect: Rect, text: String| {
let text = format!("🔥 {}", text);
let text = format!("🔥 {text}");
let color = self.style().visuals.error_fg_color;
let painter = self.debug_painter();
painter.rect_stroke(widget_rect, 0.0, (1.0, color));
@@ -612,10 +612,10 @@ impl Context {
let id_str = id.short_debug_format();
if prev_rect.min.distance(new_rect.min) < 4.0 {
show_error(new_rect, format!("Double use of {} ID {}", what, id_str));
show_error(new_rect, format!("Double use of {what} ID {id_str}"));
} else {
show_error(prev_rect, format!("First use of {} ID {}", what, id_str));
show_error(new_rect, format!("Second use of {} ID {}", what, id_str));
show_error(prev_rect, format!("First use of {what} ID {id_str}"));
show_error(new_rect, format!("Second use of {what} ID {id_str}"));
}
}
@@ -1574,14 +1574,14 @@ impl Context {
let pointer_pos = self
.pointer_hover_pos()
.map_or_else(String::new, |pos| format!("{:?}", pos));
ui.label(format!("Pointer pos: {}", pointer_pos));
.map_or_else(String::new, |pos| format!("{pos:?}"));
ui.label(format!("Pointer pos: {pointer_pos}"));
let top_layer = self
.pointer_hover_pos()
.and_then(|pos| self.layer_id_at(pos))
.map_or_else(String::new, |layer| layer.short_debug_format());
ui.label(format!("Top layer under mouse: {}", top_layer));
ui.label(format!("Top layer under mouse: {top_layer}"));
ui.add_space(16.0);
@@ -1667,7 +1667,7 @@ impl Context {
ui.image(texture_id, size);
});
ui.label(format!("{} x {}", w, h));
ui.label(format!("{w} x {h}"));
ui.label(format!("{:.3} MB", meta.bytes_used() as f64 * 1e-6));
ui.label(format!("{:?}", meta.name));
ui.end_row();
@@ -1688,8 +1688,7 @@ impl Context {
let (num_state, num_serialized) = self.data(|d| (d.len(), d.count_serialized()));
ui.label(format!(
"{} widget states stored (of which {} are serialized).",
num_state, num_serialized
"{num_state} widget states stored (of which {num_serialized} are serialized)."
));
ui.horizontal(|ui| {

View File

@@ -274,10 +274,10 @@ pub enum Event {
/// Position of the touch (or where the touch was last detected)
pos: Pos2,
/// Describes how hard the touch device was pressed. May always be `0` if the platform does
/// Describes how hard the touch device was pressed. May always be `None` if the platform does
/// not support pressure sensitivity.
/// The value is in the range from 0.0 (no pressure) to 1.0 (maximum pressure).
force: f32,
force: Option<f32>,
},
/// A raw mouse wheel event as sent by the backend (minus the z coordinate),
@@ -610,11 +610,11 @@ pub struct ModifierNames<'a> {
}
impl ModifierNames<'static> {
/// ⌥ ^ ⇧ ⌘ - NOTE: not supported by the default egui font.
/// ⌥ ⇧ ⌘ - NOTE: not supported by the default egui font.
pub const SYMBOLS: Self = Self {
is_short: true,
alt: "",
ctrl: "^",
ctrl: "",
shift: "",
mac_cmd: "",
mac_alt: "",
@@ -693,27 +693,37 @@ pub enum Key {
/// The virtual keycode for the Minus key.
Minus,
/// The virtual keycode for the Plus/Equals key.
PlusEquals,
/// Either from the main row or from the numpad.
Num0,
/// Either from the main row or from the numpad.
Num1,
/// Either from the main row or from the numpad.
Num2,
/// Either from the main row or from the numpad.
Num3,
/// Either from the main row or from the numpad.
Num4,
/// Either from the main row or from the numpad.
Num5,
/// Either from the main row or from the numpad.
Num6,
/// Either from the main row or from the numpad.
Num7,
/// Either from the main row or from the numpad.
Num8,
/// Either from the main row or from the numpad.
Num9,
@@ -906,7 +916,7 @@ fn format_kb_shortcut() {
cmd_shift_f.format(&ModifierNames::NAMES, true),
"Shift+Cmd+F"
);
assert_eq!(cmd_shift_f.format(&ModifierNames::SYMBOLS, false), "^⇧F");
assert_eq!(cmd_shift_f.format(&ModifierNames::SYMBOLS, false), "⇧F");
assert_eq!(cmd_shift_f.format(&ModifierNames::SYMBOLS, true), "⇧⌘F");
}
@@ -927,25 +937,25 @@ impl RawInput {
focused,
} = self;
ui.label(format!("screen_rect: {:?} points", screen_rect));
ui.label(format!("pixels_per_point: {:?}", pixels_per_point))
ui.label(format!("screen_rect: {screen_rect:?} points"));
ui.label(format!("pixels_per_point: {pixels_per_point:?}"))
.on_hover_text(
"Also called HDPI factor.\nNumber of physical pixels per each logical pixel.",
);
ui.label(format!("max_texture_side: {:?}", max_texture_side));
ui.label(format!("max_texture_side: {max_texture_side:?}"));
if let Some(time) = time {
ui.label(format!("time: {:.3} s", time));
ui.label(format!("time: {time:.3} s"));
} else {
ui.label("time: None");
}
ui.label(format!("predicted_dt: {:.1} ms", 1e3 * predicted_dt));
ui.label(format!("modifiers: {:#?}", modifiers));
ui.label(format!("modifiers: {modifiers:#?}"));
ui.label(format!("hovered_files: {}", hovered_files.len()));
ui.label(format!("dropped_files: {}", dropped_files.len()));
ui.label(format!("focused: {}", focused));
ui.label(format!("focused: {focused}"));
ui.scope(|ui| {
ui.set_min_height(150.0);
ui.label(format!("events: {:#?}", events))
ui.label(format!("events: {events:#?}"))
.on_hover_text("key presses etc");
});
}

View File

@@ -100,7 +100,7 @@ impl PlatformOutput {
/// This can be used by a text-to-speech system to describe the events (if any).
pub fn events_description(&self) -> String {
// only describe last event:
if let Some(event) = self.events.iter().rev().next() {
if let Some(event) = self.events.iter().next_back() {
match event {
OutputEvent::Clicked(widget_info)
| OutputEvent::DoubleClicked(widget_info)
@@ -378,7 +378,7 @@ impl Default for CursorIcon {
/// Things that happened during this frame that the integration may be interested in.
///
/// In particular, these events may be useful for accessability, i.e. for screen readers.
/// In particular, these events may be useful for accessibility, i.e. for screen readers.
#[derive(Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum OutputEvent {
@@ -417,12 +417,12 @@ impl OutputEvent {
impl std::fmt::Debug for OutputEvent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Clicked(wi) => write!(f, "Clicked({:?})", wi),
Self::DoubleClicked(wi) => write!(f, "DoubleClicked({:?})", wi),
Self::TripleClicked(wi) => write!(f, "TripleClicked({:?})", wi),
Self::FocusGained(wi) => write!(f, "FocusGained({:?})", wi),
Self::TextSelectionChanged(wi) => write!(f, "TextSelectionChanged({:?})", wi),
Self::ValueChanged(wi) => write!(f, "ValueChanged({:?})", wi),
Self::Clicked(wi) => write!(f, "Clicked({wi:?})"),
Self::DoubleClicked(wi) => write!(f, "DoubleClicked({wi:?})"),
Self::TripleClicked(wi) => write!(f, "TripleClicked({wi:?})"),
Self::FocusGained(wi) => write!(f, "FocusGained({wi:?})"),
Self::TextSelectionChanged(wi) => write!(f, "TextSelectionChanged({wi:?})"),
Self::ValueChanged(wi) => write!(f, "ValueChanged({wi:?})"),
}
}
}
@@ -609,14 +609,14 @@ impl WidgetInfo {
if let Some(selected) = selected {
if *typ == WidgetType::Checkbox {
let state = if *selected { "checked" } else { "unchecked" };
description = format!("{} {}", state, description);
description = format!("{state} {description}");
} else {
description += if *selected { "selected" } else { "" };
};
}
if let Some(label) = label {
description = format!("{}: {}", label, description);
description = format!("{label}: {description}");
}
if typ == &WidgetType::TextEdit {
@@ -630,7 +630,7 @@ impl WidgetInfo {
} else {
text = "blank".into();
}
description = format!("{}: {}", text, description);
description = format!("{text}: {description}");
}
if let Some(value) = value {

View File

@@ -1,5 +1,3 @@
use std::ops::RangeInclusive;
use crate::{id::IdSet, *};
#[derive(Clone, Copy, Debug)]
@@ -46,7 +44,7 @@ pub(crate) struct FrameState {
pub(crate) scroll_delta: Vec2, // TODO(emilk): move to `InputState` ?
/// horizontal, vertical
pub(crate) scroll_target: [Option<(RangeInclusive<f32>, Option<Align>)>; 2],
pub(crate) scroll_target: [Option<(Rangef, Option<Align>)>; 2],
#[cfg(feature = "accesskit")]
pub(crate) accesskit_state: Option<AccessKitFrameState>,

View File

@@ -47,7 +47,7 @@ impl State {
// ----------------------------------------------------------------------------
// type alias for boxed function to determine row color during grid generation
type ColorPickerFn = Box<dyn Fn(usize, &Style) -> Option<Color32>>;
type ColorPickerFn = Box<dyn Send + Sync + Fn(usize, &Style) -> Option<Color32>>;
pub(crate) struct GridLayout {
ctx: Context,
@@ -60,6 +60,7 @@ pub(crate) struct GridLayout {
/// State previous frame (if any).
/// This can be used to predict future sizes of cells.
prev_state: State,
/// State accumulated during the current frame.
curr_state: State,
initial_available: Rect,
@@ -311,7 +312,7 @@ impl Grid {
/// Setting this will allow for dynamic coloring of rows of the grid object
pub fn with_row_color<F>(mut self, color_picker: F) -> Self
where
F: Fn(usize, &Style) -> Option<Color32> + 'static,
F: Send + Sync + Fn(usize, &Style) -> Option<Color32> + 'static,
{
self.color_picker = Some(Box::new(color_picker));
self

View File

@@ -990,30 +990,28 @@ impl InputState {
});
}
ui.label(format!("scroll_delta: {:?} points", scroll_delta));
ui.label(format!("zoom_factor_delta: {:4.2}x", zoom_factor_delta));
ui.label(format!("screen_rect: {:?} points", screen_rect));
ui.label(format!("scroll_delta: {scroll_delta:?} points"));
ui.label(format!("zoom_factor_delta: {zoom_factor_delta:4.2}x"));
ui.label(format!("screen_rect: {screen_rect:?} points"));
ui.label(format!(
"{} physical pixels for each logical point",
pixels_per_point
"{pixels_per_point} physical pixels for each logical point"
));
ui.label(format!(
"max texture size (on each side): {}",
max_texture_side
"max texture size (on each side): {max_texture_side}"
));
ui.label(format!("time: {:.3} s", time));
ui.label(format!("time: {time:.3} s"));
ui.label(format!(
"time since previous frame: {:.1} ms",
1e3 * unstable_dt
));
ui.label(format!("predicted_dt: {:.1} ms", 1e3 * predicted_dt));
ui.label(format!("stable_dt: {:.1} ms", 1e3 * stable_dt));
ui.label(format!("focused: {}", focused));
ui.label(format!("modifiers: {:#?}", modifiers));
ui.label(format!("keys_down: {:?}", keys_down));
ui.label(format!("focused: {focused}"));
ui.label(format!("modifiers: {modifiers:#?}"));
ui.label(format!("keys_down: {keys_down:?}"));
ui.scope(|ui| {
ui.set_min_height(150.0);
ui.label(format!("events: {:#?}", events))
ui.label(format!("events: {events:#?}"))
.on_hover_text("key presses etc");
});
}
@@ -1037,22 +1035,21 @@ impl PointerState {
pointer_events,
} = self;
ui.label(format!("latest_pos: {:?}", latest_pos));
ui.label(format!("interact_pos: {:?}", interact_pos));
ui.label(format!("delta: {:?}", delta));
ui.label(format!("latest_pos: {latest_pos:?}"));
ui.label(format!("interact_pos: {interact_pos:?}"));
ui.label(format!("delta: {delta:?}"));
ui.label(format!(
"velocity: [{:3.0} {:3.0}] points/sec",
velocity.x, velocity.y
));
ui.label(format!("down: {:#?}", down));
ui.label(format!("press_origin: {:?}", press_origin));
ui.label(format!("press_start_time: {:?} s", press_start_time));
ui.label(format!("down: {down:#?}"));
ui.label(format!("press_origin: {press_origin:?}"));
ui.label(format!("press_start_time: {press_start_time:?} s"));
ui.label(format!(
"has_moved_too_much_for_a_click: {}",
has_moved_too_much_for_a_click
"has_moved_too_much_for_a_click: {has_moved_too_much_for_a_click}"
));
ui.label(format!("last_click_time: {:#?}", last_click_time));
ui.label(format!("last_last_click_time: {:#?}", last_last_click_time));
ui.label(format!("pointer_events: {:?}", pointer_events));
ui.label(format!("last_click_time: {last_click_time:#?}"));
ui.label(format!("last_last_click_time: {last_last_click_time:#?}"));
ui.label(format!("pointer_events: {pointer_events:?}"));
}
}

View File

@@ -118,7 +118,7 @@ struct ActiveTouch {
///
/// Note that a value of 0.0 either indicates a very light touch, or it means that the device
/// is not capable of measuring the touch force.
force: f32,
force: Option<f32>,
}
impl TouchState {
@@ -249,7 +249,7 @@ impl TouchState {
// first pass: calculate force and center of touch positions:
for touch in self.active_touches.values() {
state.avg_force += touch.force;
state.avg_force += touch.force.unwrap_or(0.0);
state.avg_pos.x += touch.pos.x;
state.avg_pos.y += touch.pos.y;
}
@@ -286,7 +286,7 @@ impl TouchState {
impl TouchState {
pub fn ui(&self, ui: &mut crate::Ui) {
ui.label(format!("{:?}", self));
ui.label(format!("{self:?}"));
}
}
@@ -294,7 +294,7 @@ impl Debug for TouchState {
// This outputs less clutter than `#[derive(Debug)]`:
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for (id, touch) in &self.active_touches {
f.write_fmt(format_args!("#{:?}: {:#?}\n", id, touch))?;
f.write_fmt(format_args!("#{id:?}: {touch:#?}\n"))?;
}
f.write_fmt(format_args!("gesture: {:#?}\n", self.gesture_state))?;
Ok(())

View File

@@ -31,10 +31,7 @@ pub(crate) fn font_texture_ui(ui: &mut Ui, [width, height]: [usize; 2]) -> Respo
Color32::BLACK
};
ui.label(format!(
"Texture size: {} x {} (hover to zoom)",
width, height
));
ui.label(format!("Texture size: {width} x {height} (hover to zoom)"));
if width <= 1 || height <= 1 {
return;
}
@@ -108,7 +105,7 @@ impl Widget for &epaint::stats::PaintStats {
label(ui, shape_path, "paths");
label(ui, shape_mesh, "nested meshes");
label(ui, shape_vec, "nested shapes");
ui.label(format!("{:6} callbacks", num_callbacks));
ui.label(format!("{num_callbacks:6} callbacks"));
ui.add_space(10.0);
ui.label("Text shapes:");

View File

@@ -127,7 +127,7 @@ impl PaintList {
#[inline(always)]
pub fn add(&mut self, clip_rect: Rect, shape: Shape) -> ShapeIdx {
let idx = ShapeIdx(self.0.len());
self.0.push(ClippedShape(clip_rect, shape));
self.0.push(ClippedShape { clip_rect, shape });
idx
}
@@ -135,7 +135,7 @@ impl PaintList {
self.0.extend(
shapes
.into_iter()
.map(|shape| ClippedShape(clip_rect, shape)),
.map(|shape| ClippedShape { clip_rect, shape }),
);
}
@@ -148,12 +148,12 @@ impl PaintList {
/// and then later setting it using `paint_list.set(idx, cr, frame);`.
#[inline(always)]
pub fn set(&mut self, idx: ShapeIdx, clip_rect: Rect, shape: Shape) {
self.0[idx.0] = ClippedShape(clip_rect, shape);
self.0[idx.0] = ClippedShape { clip_rect, shape };
}
/// Translate each [`Shape`] and clip rectangle by this much, in-place
pub fn translate(&mut self, delta: Vec2) {
for ClippedShape(clip_rect, shape) in &mut self.0 {
for ClippedShape { clip_rect, shape } in &mut self.0 {
*clip_rect = clip_rect.translate(delta);
shape.translate(delta);
}

View File

@@ -335,7 +335,9 @@ pub use epaint::emath;
#[cfg(feature = "color-hex")]
pub use ecolor::hex_color;
pub use ecolor::{Color32, Rgba};
pub use emath::{lerp, pos2, remap, remap_clamp, vec2, Align, Align2, NumExt, Pos2, Rect, Vec2};
pub use emath::{
lerp, pos2, remap, remap_clamp, vec2, Align, Align2, NumExt, Pos2, Rangef, Rect, Vec2,
};
pub use epaint::{
mutex,
text::{FontData, FontDefinitions, FontFamily, FontId, FontTweak},

View File

@@ -546,8 +546,10 @@ impl Memory {
#[cfg_attr(feature = "serde", serde(default))]
pub struct Areas {
areas: IdMap<area::State>,
/// Back-to-front. Top is last.
order: Vec<LayerId>,
visible_last_frame: ahash::HashSet<LayerId>,
visible_current_frame: ahash::HashSet<LayerId>,

View File

@@ -1,8 +1,7 @@
use std::ops::RangeInclusive;
use std::sync::Arc;
use crate::{
emath::{Align2, Pos2, Rect, Vec2},
emath::{Align2, Pos2, Rangef, Rect, Vec2},
layers::{LayerId, PaintList, ShapeIdx},
Color32, Context, FontId,
};
@@ -227,7 +226,7 @@ impl Painter {
pub fn error(&self, pos: Pos2, text: impl std::fmt::Display) -> Rect {
let color = self.ctx.style().visuals.error_fg_color;
self.debug_text(pos, Align2::LEFT_TOP, color, format!("🔥 {}", text))
self.debug_text(pos, Align2::LEFT_TOP, color, format!("🔥 {text}"))
}
/// text with a background
@@ -263,12 +262,12 @@ impl Painter {
}
/// Paints a horizontal line.
pub fn hline(&self, x: RangeInclusive<f32>, y: f32, stroke: impl Into<Stroke>) {
pub fn hline(&self, x: impl Into<Rangef>, y: f32, stroke: impl Into<Stroke>) {
self.add(Shape::hline(x, y, stroke));
}
/// Paints a vertical line.
pub fn vline(&self, x: f32, y: RangeInclusive<f32>, stroke: impl Into<Stroke>) {
pub fn vline(&self, x: f32, y: impl Into<Rangef>, stroke: impl Into<Stroke>) {
self.add(Shape::vline(x, y, stroke));
}

View File

@@ -360,30 +360,46 @@ impl Margin {
}
/// Total margins on both sides
#[inline]
pub fn sum(&self) -> Vec2 {
vec2(self.left + self.right, self.top + self.bottom)
}
#[inline]
pub fn left_top(&self) -> Vec2 {
vec2(self.left, self.top)
}
#[inline]
pub fn right_bottom(&self) -> Vec2 {
vec2(self.right, self.bottom)
}
#[inline]
pub fn is_same(&self) -> bool {
self.left == self.right && self.left == self.top && self.left == self.bottom
}
#[inline]
pub fn expand_rect(&self, rect: Rect) -> Rect {
Rect::from_min_max(rect.min - self.left_top(), rect.max + self.right_bottom())
}
#[inline]
pub fn shrink_rect(&self, rect: Rect) -> Rect {
Rect::from_min_max(rect.min + self.left_top(), rect.max - self.right_bottom())
}
}
impl From<f32> for Margin {
#[inline]
fn from(v: f32) -> Self {
Self::same(v)
}
}
impl From<Vec2> for Margin {
#[inline]
fn from(v: Vec2) -> Self {
Self::symmetric(v.x, v.y)
}
@@ -392,6 +408,7 @@ impl From<Vec2> for Margin {
impl std::ops::Add for Margin {
type Output = Self;
#[inline]
fn add(self, other: Self) -> Self {
Self {
left: self.left + other.left,
@@ -491,7 +508,8 @@ pub struct Visuals {
pub resize_corner_size: f32,
pub text_cursor_width: f32,
/// The color and width of the text cursor
pub text_cursor: Stroke,
/// show where the text cursor would be if you clicked
pub text_cursor_preview: bool,
@@ -767,7 +785,7 @@ impl Visuals {
popup_shadow: Shadow::small_dark(),
resize_corner_size: 12.0,
text_cursor_width: 2.0,
text_cursor: Stroke::new(2.0, Color32::from_rgb(192, 222, 255)),
text_cursor_preview: false,
clip_rect_margin: 3.0, // should be at least half the size of the widest frame stroke + max WidgetVisuals::expansion
button_frame: true,
@@ -800,6 +818,7 @@ impl Visuals {
panel_fill: Color32::from_gray(248),
popup_shadow: Shadow::small_light(),
text_cursor: Stroke::new(2.0, Color32::from_rgb(0, 83, 125)),
..Self::dark()
}
}
@@ -1334,7 +1353,7 @@ impl Visuals {
popup_shadow,
resize_corner_size,
text_cursor_width,
text_cursor,
text_cursor_preview,
clip_rect_margin,
button_frame,
@@ -1392,8 +1411,9 @@ impl Visuals {
});
ui_color(ui, hyperlink_color, "hyperlink_color");
stroke_ui(ui, text_cursor, "Text Cursor");
ui.add(Slider::new(resize_corner_size, 0.0..=20.0).text("resize_corner_size"));
ui.add(Slider::new(text_cursor_width, 0.0..=4.0).text("text_cursor_width"));
ui.checkbox(text_cursor_preview, "Preview text cursor on hover");
ui.add(Slider::new(clip_rect_margin, 0.0..=20.0).text("clip_rect_margin"));

View File

@@ -517,15 +517,17 @@ impl Ui {
}
/// `ui.set_width_range(min..=max);` is equivalent to `ui.set_min_width(min); ui.set_max_width(max);`.
pub fn set_width_range(&mut self, width: std::ops::RangeInclusive<f32>) {
self.set_min_width(*width.start());
self.set_max_width(*width.end());
pub fn set_width_range(&mut self, width: impl Into<Rangef>) {
let width = width.into();
self.set_min_width(width.min);
self.set_max_width(width.max);
}
/// `ui.set_height_range(min..=max);` is equivalent to `ui.set_min_height(min); ui.set_max_height(max);`.
pub fn set_height_range(&mut self, height: std::ops::RangeInclusive<f32>) {
self.set_min_height(*height.start());
self.set_max_height(*height.end());
pub fn set_height_range(&mut self, height: impl Into<Rangef>) {
let height = height.into();
self.set_min_height(height.min);
self.set_max_height(height.max);
}
/// Set both the minimum and maximum width.
@@ -556,6 +558,7 @@ impl Ui {
// Layout related measures:
/// The available space at the moment, given the current cursor.
///
/// This how much more space we can take up without overflowing our parent.
/// Shrinks as widgets allocate space and the cursor moves.
/// A small size should be interpreted as "as little as possible".
@@ -564,19 +567,30 @@ impl Ui {
self.placer.available_size()
}
/// The available width at the moment, given the current cursor.
///
/// See [`Self::available_size`] for more information.
pub fn available_width(&self) -> f32 {
self.available_size().x
}
/// The available height at the moment, given the current cursor.
///
/// See [`Self::available_size`] for more information.
pub fn available_height(&self) -> f32 {
self.available_size().y
}
/// In case of a wrapping layout, how much space is left on this row/column?
///
/// If the layout does not wrap, this will return the same value as [`Self::available_size`].
pub fn available_size_before_wrap(&self) -> Vec2 {
self.placer.available_rect_before_wrap().size()
}
/// In case of a wrapping layout, how much space is left on this row/column?
///
/// If the layout does not wrap, this will return the same value as [`Self::available_size`].
pub fn available_rect_before_wrap(&self) -> Rect {
self.placer.available_rect_before_wrap()
}
@@ -966,7 +980,7 @@ impl Ui {
/// ```
pub fn scroll_to_rect(&self, rect: Rect, align: Option<Align>) {
for d in 0..2 {
let range = rect.min[d]..=rect.max[d];
let range = Rangef::new(rect.min[d], rect.max[d]);
self.ctx()
.frame_state_mut(|state| state.scroll_target[d] = Some((range, align)));
}
@@ -996,9 +1010,9 @@ impl Ui {
pub fn scroll_to_cursor(&self, align: Option<Align>) {
let target = self.next_widget_position();
for d in 0..2 {
let target = target[d];
let target = Rangef::point(target[d]);
self.ctx()
.frame_state_mut(|state| state.scroll_target[d] = Some((target..=target, align)));
.frame_state_mut(|state| state.scroll_target[d] = Some((target, align)));
}
}
@@ -2231,3 +2245,9 @@ impl Ui {
}
}
}
#[test]
fn ui_impl_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Ui>();
}

View File

@@ -676,7 +676,7 @@ impl WidgetTextGalley {
self.galley.size()
}
/// Size of the laid out text.
/// The full, non-elided text of the input job.
#[inline]
pub fn text(&self) -> &str {
self.galley.text()

View File

@@ -23,6 +23,7 @@ pub struct Button {
text: WidgetText,
shortcut_text: WidgetText,
wrap: Option<bool>,
/// None means default for interact
fill: Option<Color32>,
stroke: Option<Stroke>,

View File

@@ -234,17 +234,17 @@ fn color_text_ui(ui: &mut Ui, color: impl Into<Color32>, alpha: Alpha) {
if ui.button("📋").on_hover_text("Click to copy").clicked() {
if alpha == Alpha::Opaque {
ui.output_mut(|o| o.copied_text = format!("{}, {}, {}", r, g, b));
ui.output_mut(|o| o.copied_text = format!("{r}, {g}, {b}"));
} else {
ui.output_mut(|o| o.copied_text = format!("{}, {}, {}, {}", r, g, b, a));
ui.output_mut(|o| o.copied_text = format!("{r}, {g}, {b}, {a}"));
}
}
if alpha == Alpha::Opaque {
ui.label(format!("rgb({}, {}, {})", r, g, b))
ui.label(format!("rgb({r}, {g}, {b})"))
.on_hover_text("Red Green Blue");
} else {
ui.label(format!("rgba({}, {}, {}, {})", r, g, b, a))
ui.label(format!("rgba({r}, {g}, {b}, {a})"))
.on_hover_text("Red Green Blue with premultiplied Alpha");
}
});

View File

@@ -11,6 +11,7 @@ use crate::*;
pub(crate) struct MonoState {
last_dragged_id: Option<Id>,
last_dragged_value: Option<f64>,
/// For temporary edit of a [`DragValue`] value.
/// Couples with the current focus id.
edit_string: Option<String>,
@@ -63,6 +64,7 @@ pub struct DragValue<'a> {
max_decimals: Option<usize>,
custom_formatter: Option<NumFormatter<'a>>,
custom_parser: Option<NumParser<'a>>,
update_while_editing: bool,
}
impl<'a> DragValue<'a> {
@@ -94,6 +96,7 @@ impl<'a> DragValue<'a> {
max_decimals: None,
custom_formatter: None,
custom_parser: None,
update_while_editing: true,
}
}
@@ -352,6 +355,15 @@ impl<'a> DragValue<'a> {
}
.custom_parser(|s| i64::from_str_radix(s, 16).map(|n| n as f64).ok())
}
/// 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.
pub fn update_while_editing(mut self, update: bool) -> Self {
self.update_while_editing = update;
self
}
}
impl<'a> Widget for DragValue<'a> {
@@ -366,6 +378,7 @@ impl<'a> Widget for DragValue<'a> {
max_decimals,
custom_formatter,
custom_parser,
update_while_editing,
} = self;
let shift = ui.input(|i| i.modifiers.shift_only());
@@ -392,7 +405,9 @@ impl<'a> Widget for DragValue<'a> {
let auto_decimals = (aim_rad / speed.abs()).log10().ceil().clamp(0.0, 15.0) as usize;
let auto_decimals = auto_decimals + is_slow_speed as usize;
let max_decimals = max_decimals.unwrap_or(auto_decimals + 2);
let max_decimals = max_decimals
.unwrap_or(auto_decimals + 2)
.at_least(min_decimals);
let auto_decimals = auto_decimals.clamp(min_decimals, max_decimals);
let change = ui.input_mut(|input| {
@@ -475,9 +490,15 @@ impl<'a> Widget for DragValue<'a> {
.desired_width(ui.spacing().interact_size.x)
.font(text_style),
);
// Only update the value when the user presses enter, or clicks elsewhere. NOT every frame.
// See https://github.com/emilk/egui/issues/2687
if response.lost_focus() {
let update = if update_while_editing {
// Update when the edit content has changed.
response.changed()
} else {
// Update only when the edit has lost focus.
response.lost_focus()
};
if update {
let parsed_value = match custom_parser {
Some(parser) => parser(&value_text),
None => value_text.parse().ok(),
@@ -606,7 +627,7 @@ impl<'a> Widget for DragValue<'a> {
// The value is exposed as a string by the text edit widget
// when in edit mode.
if !is_kb_editing {
let value_text = format!("{}{}{}", prefix, value_text, suffix);
let value_text = format!("{prefix}{value_text}{suffix}");
builder.set_value(value_text);
}
});

View File

@@ -83,6 +83,7 @@ impl Widget for Link {
pub struct Hyperlink {
url: String,
text: WidgetText,
new_tab: bool,
}
impl Hyperlink {
@@ -92,6 +93,7 @@ impl Hyperlink {
Self {
url: url.clone(),
text: url.into(),
new_tab: false,
}
}
@@ -100,13 +102,20 @@ impl Hyperlink {
Self {
url: url.to_string(),
text: text.into(),
new_tab: false,
}
}
/// Always open this hyperlink in a new browser tab.
pub fn open_in_new_tab(mut self, new_tab: bool) -> Self {
self.new_tab = new_tab;
self
}
}
impl Widget for Hyperlink {
fn ui(self, ui: &mut Ui) -> Response {
let Self { url, text } = self;
let Self { url, text, new_tab } = self;
let response = ui.add(Link::new(text));
if response.clicked() {
@@ -114,7 +123,7 @@ impl Widget for Hyperlink {
ui.ctx().output_mut(|o| {
o.open_url = Some(crate::output::OpenUrl {
url: url.clone(),
new_tab: modifiers.any(),
new_tab: new_tab || modifiers.any(),
});
});
}

View File

@@ -12,10 +12,14 @@ use crate::{widget_text::WidgetTextGalley, *};
/// ui.label(egui::RichText::new("With formatting").underline());
/// # });
/// ```
///
/// For full control of the text you can use [`crate::text::LayoutJob`]
/// as argument to [`Self::new`].
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
pub struct Label {
text: WidgetText,
wrap: Option<bool>,
truncate: bool,
sense: Option<Sense>,
}
@@ -24,6 +28,7 @@ impl Label {
Self {
text: text.into(),
wrap: None,
truncate: false,
sense: None,
}
}
@@ -34,6 +39,8 @@ impl Label {
/// If `true`, the text will wrap to stay within the max width of the [`Ui`].
///
/// Calling `wrap` will override [`Self::truncate`].
///
/// By default [`Self::wrap`] will be `true` in vertical layouts
/// and horizontal layouts with wrapping,
/// and `false` on non-wrapping horizontal layouts.
@@ -44,6 +51,23 @@ impl Label {
#[inline]
pub fn wrap(mut self, wrap: bool) -> Self {
self.wrap = Some(wrap);
self.truncate = false;
self
}
/// If `true`, the text will stop at the max width of the [`Ui`],
/// and what doesn't fit will be elided, replaced with `…`.
///
/// If the text is truncated, the full text will be shown on hover as a tool-tip.
///
/// Default is `false`, which means the text will expand the parent [`Ui`],
/// or wrap if [`Self::wrap`] is set.
///
/// Calling `truncate` will override [`Self::wrap`].
#[inline]
pub fn truncate(mut self, truncate: bool) -> Self {
self.wrap = None;
self.truncate = truncate;
self
}
@@ -98,10 +122,11 @@ impl Label {
.text
.into_text_job(ui.style(), FontSelection::Default, valign);
let should_wrap = self.wrap.unwrap_or_else(|| ui.wrap_text());
let truncate = self.truncate;
let wrap = !truncate && self.wrap.unwrap_or_else(|| ui.wrap_text());
let available_width = ui.available_width();
if should_wrap
if wrap
&& ui.layout().main_dir() == Direction::LeftToRight
&& ui.layout().main_wrap()
&& available_width.is_finite()
@@ -138,7 +163,11 @@ impl Label {
}
(pos, text_galley, response)
} else {
if should_wrap {
if truncate {
text_job.job.wrap.max_width = available_width;
text_job.job.wrap.max_rows = 1;
text_job.job.wrap.break_anywhere = true;
} else if wrap {
text_job.job.wrap.max_width = available_width;
} else {
text_job.job.wrap.max_width = f32::INFINITY;
@@ -167,9 +196,14 @@ impl Label {
impl Widget for Label {
fn ui(self, ui: &mut Ui) -> Response {
let (pos, text_galley, response) = self.layout_in_ui(ui);
let (pos, text_galley, mut response) = self.layout_in_ui(ui);
response.widget_info(|| WidgetInfo::labeled(WidgetType::Label, text_galley.text()));
if text_galley.galley.elided {
// Show the full (non-elided) text on hover:
response = response.on_hover_text(text_galley.text());
}
if ui.is_rect_visible(response.rect) {
let response_color = ui.style().interact(&response).text_color();

View File

@@ -85,13 +85,13 @@ impl<const AXIS: usize> AxisHints<AXIS> {
fn default_formatter(tick: f64, max_digits: usize, _range: &RangeInclusive<f64>) -> String {
if tick.abs() > 10.0_f64.powf(max_digits as f64) {
let tick_rounded = tick as isize;
return format!("{:+e}", tick_rounded);
return format!("{tick_rounded:+e}");
}
let tick_rounded = round_to_decimals(tick, max_digits);
if tick.abs() < 10.0_f64.powf(-(max_digits as f64)) && tick != 0.0 {
return format!("{:+e}", tick_rounded);
return format!("{tick_rounded:+e}");
}
format!("{}", tick_rounded)
tick_rounded.to_string()
}
/// Specify axis label.

View File

@@ -760,15 +760,22 @@ impl PlotItem for Text {
/// A set of points.
pub struct Points {
pub(super) series: PlotPoints,
pub(super) shape: MarkerShape,
/// Color of the marker. `Color32::TRANSPARENT` means that it will be picked automatically.
pub(super) color: Color32,
/// Whether to fill the marker. Does not apply to all types.
pub(super) filled: bool,
/// The maximum extent of the marker from its center.
pub(super) radius: f32,
pub(super) name: String,
pub(super) highlight: bool,
pub(super) stems: Option<f32>,
}
@@ -997,6 +1004,7 @@ impl PlotItem for Points {
pub struct Arrows {
pub(super) origins: PlotPoints,
pub(super) tips: PlotPoints,
pub(super) tip_length: Option<f32>,
pub(super) color: Color32,
pub(super) name: String,
pub(super) highlight: bool,
@@ -1007,6 +1015,7 @@ impl Arrows {
Self {
origins: origins.into(),
tips: tips.into(),
tip_length: None,
color: Color32::TRANSPARENT,
name: Default::default(),
highlight: false,
@@ -1019,6 +1028,12 @@ impl Arrows {
self
}
/// Set the length of the arrow tips
pub fn tip_length(mut self, tip_length: f32) -> Self {
self.tip_length = Some(tip_length);
self
}
/// Set the arrows' color.
pub fn color(mut self, color: impl Into<Color32>) -> Self {
self.color = color.into();
@@ -1044,6 +1059,7 @@ impl PlotItem for Arrows {
let Self {
origins,
tips,
tip_length,
color,
highlight,
..
@@ -1062,7 +1078,11 @@ impl PlotItem for Arrows {
.for_each(|(origin, tip)| {
let vector = tip - origin;
let rot = Rot2::from_angle(std::f32::consts::TAU / 10.0);
let tip_length = vector.length() / 4.0;
let tip_length = if let Some(tip_length) = tip_length {
*tip_length
} else {
vector.length() / 4.0
};
let tip = origin + vector;
let dir = vector.normalized();
shapes.push(Shape::line_segment([origin, tip], stroke));
@@ -1119,6 +1139,7 @@ pub struct PlotImage {
pub(super) tint: Color32,
pub(super) highlight: bool,
pub(super) name: String,
pub(crate) rotation: Option<(f32, Vec2)>,
}
impl PlotImage {
@@ -1137,6 +1158,7 @@ impl PlotImage {
size: size.into(),
bg_fill: Default::default(),
tint: Color32::WHITE,
rotation: None,
}
}
@@ -1175,6 +1197,17 @@ impl PlotImage {
self.name = name.to_string();
self
}
/// Rotate the image about an origin by some angle
///
/// Positive angle is clockwise.
/// Origin is a vector in normalized UV space ((0,0) in top-left, (1,1) bottom right).
///
/// To rotate about the center you can pass `Vec2::splat(0.5)` as the origin.
pub fn rotate(mut self, angle: f32, origin: Vec2) -> Self {
self.rotation = Some((angle, origin));
self
}
}
impl PlotItem for PlotImage {
@@ -1202,11 +1235,14 @@ impl PlotItem for PlotImage {
let right_bottom_tf = transform.position_from_point(&right_bottom);
Rect::from_two_pos(left_top_tf, right_bottom_tf)
};
Image::new(*texture_id, *size)
let mut image = Image::new(*texture_id, *size)
.bg_fill(*bg_fill)
.tint(*tint)
.uv(*uv)
.paint_at(ui, rect);
.uv(*uv);
if let Some((angle, origin)) = self.rotation {
image = image.rotate(angle, origin);
}
image.paint_at(ui, rect);
if *highlight {
shapes.push(Shape::rect_stroke(
rect,
@@ -1261,8 +1297,10 @@ pub struct BarChart {
pub(super) bars: Vec<Bar>,
pub(super) default_color: Color32,
pub(super) name: String,
/// A custom element formatter
pub(super) element_formatter: Option<Box<dyn Fn(&Bar, &BarChart) -> String>>,
highlight: bool,
}
@@ -1431,8 +1469,10 @@ pub struct BoxPlot {
pub(super) boxes: Vec<BoxElem>,
pub(super) default_color: Color32,
pub(super) name: String,
/// A custom element formatter
pub(super) element_formatter: Option<Box<dyn Fn(&BoxElem, &BoxPlot) -> String>>,
highlight: bool,
}
@@ -1692,7 +1732,7 @@ pub(super) fn rulers_at_value(
let mut prefix = String::new();
if !name.is_empty() {
prefix = format!("{}\n", name);
prefix = format!("{name}\n");
}
let text = {

View File

@@ -125,8 +125,8 @@ impl ToString for LineStyle {
fn to_string(&self) -> String {
match self {
LineStyle::Solid => "Solid".into(),
LineStyle::Dotted { spacing } => format!("Dotted{}Px", spacing),
LineStyle::Dashed { length } => format!("Dashed{}Px", length),
LineStyle::Dotted { spacing } => format!("Dotted{spacing}Px"),
LineStyle::Dashed { length } => format!("Dashed{length}Px"),
}
}
}

View File

@@ -103,9 +103,11 @@ struct PlotMemory {
/// Indicates if the user has modified the bounds, for example by moving or zooming,
/// or if the bounds should be calculated based by included point or auto bounds.
bounds_modified: AxisBools,
hovered_entry: Option<String>,
hidden_items: ahash::HashSet<String>,
last_plot_transform: PlotTransform,
/// Allows to remember the first click position when performing a boxed zoom
last_click_pos_for_zoom: Option<Pos2>,
}
@@ -1088,7 +1090,7 @@ impl Plot {
delta.y = 0.0;
}
transform.translate_bounds(delta);
bounds_modified = true.into();
bounds_modified = allow_drag;
}
// Zooming
@@ -1159,7 +1161,7 @@ impl Plot {
}
if zoom_factor != Vec2::splat(1.0) {
transform.zoom(zoom_factor, hover_pos);
bounds_modified = true.into();
bounds_modified = allow_zoom;
}
}
if allow_scroll {
@@ -1338,17 +1340,25 @@ impl PlotUi {
.push(BoundsModification::Translate(delta_pos));
}
/// Can be used to check if the plot was hovered or clicked.
pub fn response(&self) -> &Response {
&self.response
}
/// Returns `true` if the plot area is currently hovered.
#[deprecated = "Use plot_ui.response().hovered()"]
pub fn plot_hovered(&self) -> bool {
self.response.hovered()
}
/// Returns `true` if the plot was clicked by the primary button.
#[deprecated = "Use plot_ui.response().clicked()"]
pub fn plot_clicked(&self) -> bool {
self.response.clicked()
}
/// Returns `true` if the plot was clicked by the secondary button.
#[deprecated = "Use plot_ui.response().secondary_clicked()"]
pub fn plot_secondary_clicked(&self) -> bool {
self.response.secondary_clicked()
}
@@ -1873,7 +1883,7 @@ pub fn format_number(number: f64, num_decimals: usize) -> String {
let is_integral = number as i64 as f64 == number;
if is_integral {
// perfect integer - show it as such:
format!("{:.0}", number)
format!("{number:.0}")
} else {
// make sure we tell the user it is not an integer by always showing a decimal or two:
format!("{:.*}", num_decimals.at_least(1), number)

View File

@@ -77,8 +77,10 @@ pub struct Slider<'a> {
prefix: String,
suffix: String,
text: WidgetText,
/// Sets the minimal step of the widget value
step: Option<f64>,
drag_value_speed: Option<f64>,
min_decimals: usize,
max_decimals: Option<usize>,
@@ -524,12 +526,12 @@ impl<'a> Slider<'a> {
}
/// For instance, `position` is the mouse position and `position_range` is the physical location of the slider on the screen.
fn value_from_position(&self, position: f32, position_range: RangeInclusive<f32>) -> f64 {
fn value_from_position(&self, position: f32, position_range: Rangef) -> f64 {
let normalized = remap_clamp(position, position_range, 0.0..=1.0) as f64;
value_from_normalized(normalized, self.range(), &self.spec)
}
fn position_from_value(&self, value: f64, position_range: RangeInclusive<f32>) -> f32 {
fn position_from_value(&self, value: f64, position_range: Rangef) -> f32 {
let normalized = normalized_from_value(value, self.range(), &self.spec);
lerp(position_range, normalized as f32)
}
@@ -555,11 +557,11 @@ impl<'a> Slider<'a> {
let new_value = if self.smart_aim {
let aim_radius = ui.input(|i| i.aim_radius());
emath::smart_aim::best_in_range_f64(
self.value_from_position(position - aim_radius, position_range.clone()),
self.value_from_position(position + aim_radius, position_range.clone()),
self.value_from_position(position - aim_radius, position_range),
self.value_from_position(position + aim_radius, position_range),
)
} else {
self.value_from_position(position, position_range.clone())
self.value_from_position(position, position_range)
};
self.set_value(new_value);
}
@@ -594,18 +596,18 @@ impl<'a> Slider<'a> {
if kb_step != 0.0 {
let prev_value = self.get_value();
let prev_position = self.position_from_value(prev_value, position_range.clone());
let prev_position = self.position_from_value(prev_value, position_range);
let new_position = prev_position + kb_step;
let new_value = match self.step {
Some(step) => prev_value + (kb_step as f64 * step),
None if self.smart_aim => {
let aim_radius = ui.input(|i| i.aim_radius());
emath::smart_aim::best_in_range_f64(
self.value_from_position(new_position - aim_radius, position_range.clone()),
self.value_from_position(new_position + aim_radius, position_range.clone()),
self.value_from_position(new_position - aim_radius, position_range),
self.value_from_position(new_position + aim_radius, position_range),
)
}
_ => self.value_from_position(new_position, position_range.clone()),
_ => self.value_from_position(new_position, position_range),
};
self.set_value(new_value);
}
@@ -686,15 +688,11 @@ impl<'a> Slider<'a> {
}
}
fn position_range(&self, rect: &Rect) -> RangeInclusive<f32> {
fn position_range(&self, rect: &Rect) -> Rangef {
let handle_radius = self.handle_radius(rect);
match self.orientation {
SliderOrientation::Horizontal => {
(rect.left() + handle_radius)..=(rect.right() - handle_radius)
}
SliderOrientation::Vertical => {
(rect.bottom() - handle_radius)..=(rect.top() + handle_radius)
}
SliderOrientation::Horizontal => rect.x_range().shrink(handle_radius),
SliderOrientation::Vertical => rect.y_range().shrink(handle_radius),
}
}
@@ -726,7 +724,7 @@ impl<'a> Slider<'a> {
}
}
fn value_ui(&mut self, ui: &mut Ui, position_range: RangeInclusive<f32>) -> Response {
fn value_ui(&mut self, ui: &mut Ui, position_range: Rangef) -> Response {
// If [`DragValue`] is controlled from the keyboard and `step` is defined, set speed to `step`
let change = ui.input(|input| {
input.num_presses(Key::ArrowUp) as i32 + input.num_presses(Key::ArrowRight) as i32
@@ -740,7 +738,7 @@ impl<'a> Slider<'a> {
step
} else {
self.drag_value_speed
.unwrap_or_else(|| self.current_gradient(&position_range))
.unwrap_or_else(|| self.current_gradient(position_range))
};
let mut value = self.get_value();
@@ -767,12 +765,11 @@ impl<'a> Slider<'a> {
}
/// delta(value) / delta(points)
fn current_gradient(&mut self, position_range: &RangeInclusive<f32>) -> f64 {
fn current_gradient(&mut self, position_range: Rangef) -> f64 {
// TODO(emilk): handle clamping
let value = self.get_value();
let value_from_pos =
|position: f32| self.value_from_position(position, position_range.clone());
let pos_from_value = |value: f64| self.position_from_value(value, position_range.clone());
let value_from_pos = |position: f32| self.value_from_position(position, position_range);
let pos_from_value = |value: f64| self.position_from_value(value, position_range);
let left_value = value_from_pos(pos_from_value(value) - 0.5);
let right_value = value_from_pos(pos_from_value(value) + 0.5);
right_value - left_value

View File

@@ -1138,7 +1138,7 @@ fn paint_cursor_end(
galley: &Galley,
cursor: &Cursor,
) -> Rect {
let stroke = ui.visuals().selection.stroke;
let stroke = ui.visuals().text_cursor;
let mut cursor_pos = galley.pos_from_cursor(cursor).translate(pos.to_vec2());
cursor_pos.max.y = cursor_pos.max.y.at_least(cursor_pos.min.y + row_height); // Handle completely empty galleys
@@ -1147,10 +1147,7 @@ fn paint_cursor_end(
let top = cursor_pos.center_top();
let bottom = cursor_pos.center_bottom();
painter.line_segment(
[top, bottom],
(ui.visuals().text_cursor_width, stroke.color),
);
painter.line_segment([top, bottom], (stroke.width, stroke.color));
if false {
// Roof/floor:
@@ -1185,7 +1182,7 @@ fn insert_text(
if char_limit < usize::MAX {
let mut new_string = text_to_insert;
// Avoid subtract with overflow panic
let cutoff = char_limit.saturating_sub(text.as_str().len());
let cutoff = char_limit.saturating_sub(text.as_str().chars().count());
new_string = match new_string.char_indices().nth(cutoff) {
None => new_string,

View File

@@ -4,7 +4,7 @@ version = "0.22.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.65"
rust-version = "1.67"
publish = false
default-run = "egui_demo_app"
@@ -65,6 +65,6 @@ env_logger = "0.10"
# web:
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen = "=0.2.86"
wasm-bindgen = "=0.2.87"
wasm-bindgen-futures = "0.4"
web-sys = "0.3"

View File

@@ -133,7 +133,7 @@ fn ui_url(ui: &mut egui::Ui, frame: &mut eframe::Frame, url: &mut String) -> boo
if ui.button("Random image").clicked() {
let seed = ui.input(|i| i.time);
let side = 640;
*url = format!("https://picsum.photos/seed/{}/{}", seed, side);
*url = format!("https://picsum.photos/seed/{seed}/{side}");
trigger_fetch = true;
}
});

View File

@@ -232,8 +232,7 @@ impl BackendPanel {
if ui
.add_enabled(enabled, egui::Button::new("Reset"))
.on_hover_text(format!(
"Reset scale to native value ({:.1})",
native_pixels_per_point
"Reset scale to native value ({native_pixels_per_point:.1})"
))
.clicked()
{
@@ -441,7 +440,7 @@ impl EguiWindows {
.stick_to_bottom(true)
.show(ui, |ui| {
for event in output_event_history {
ui.label(format!("{:?}", event));
ui.label(format!("{event:?}"));
}
});
});

View File

@@ -354,7 +354,7 @@ impl WrapApp {
{
selected_anchor = anchor;
if frame.is_web() {
ui.output_mut(|o| o.open_url(format!("#{}", anchor)));
ui.output_mut(|o| o.open_url(format!("#{anchor}")));
}
}
}

View File

@@ -4,7 +4,7 @@ version = "0.22.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "Example library for egui"
edition = "2021"
rust-version = "1.65"
rust-version = "1.67"
homepage = "https://github.com/emilk/egui/tree/master/crates/egui_demo_lib"
license = "MIT OR Apache-2.0"
readme = "README.md"

View File

@@ -83,11 +83,11 @@ fn about_immediate_mode(ui: &mut egui::Ui) {
fn links(ui: &mut egui::Ui) {
use egui::special_emojis::{GITHUB, TWITTER};
ui.hyperlink_to(
format!("{} egui on GitHub", GITHUB),
format!("{GITHUB} egui on GitHub"),
"https://github.com/emilk/egui",
);
ui.hyperlink_to(
format!("{} @ernerfeldt", TWITTER),
format!("{TWITTER} @ernerfeldt"),
"https://twitter.com/ernerfeldt",
);
ui.hyperlink_to("egui documentation", "https://docs.rs/egui/");

View File

@@ -121,7 +121,7 @@ impl CodeExample {
ui.separator();
code_view_ui(ui, &format!("{:#?}", self));
code_view_ui(ui, &format!("{self:#?}"));
ui.separator();

View File

@@ -251,11 +251,11 @@ impl DemoWindows {
use egui::special_emojis::{GITHUB, TWITTER};
ui.hyperlink_to(
format!("{} egui on GitHub", GITHUB),
format!("{GITHUB} egui on GitHub"),
"https://github.com/emilk/egui",
);
ui.hyperlink_to(
format!("{} @ernerfeldt", TWITTER),
format!("{TWITTER} @ernerfeldt"),
"https://twitter.com/ernerfeldt",
);

View File

@@ -140,7 +140,7 @@ impl LayoutTest {
Direction::TopDown,
Direction::BottomUp,
] {
ui.radio_value(&mut self.layout.main_dir, dir, format!("{:?}", dir));
ui.radio_value(&mut self.layout.main_dir, dir, format!("{dir:?}"));
}
});
@@ -162,7 +162,7 @@ impl LayoutTest {
ui.horizontal(|ui| {
ui.label("Cross Align:");
for &align in &[Align::Min, Align::Center, Align::Max] {
ui.radio_value(&mut self.layout.cross_align, align, format!("{:?}", align));
ui.radio_value(&mut self.layout.cross_align, align, format!("{align:?}"));
}
});

View File

@@ -1,5 +1,4 @@
use super::*;
use crate::LOREM_IPSUM;
use egui::{epaint::text::TextWrapping, *};
/// Showcase some ui code
@@ -8,9 +7,7 @@ use egui::{epaint::text::TextWrapping, *};
pub struct MiscDemoWindow {
num_columns: usize,
break_anywhere: bool,
max_rows: usize,
overflow_character: Option<char>,
text_break: TextBreakDemo,
widgets: Widgets,
colors: ColorWidgets,
@@ -27,9 +24,7 @@ impl Default for MiscDemoWindow {
MiscDemoWindow {
num_columns: 2,
max_rows: 2,
break_anywhere: false,
overflow_character: Some('…'),
text_break: Default::default(),
widgets: Default::default(),
colors: Default::default(),
@@ -61,8 +56,14 @@ impl View for MiscDemoWindow {
fn ui(&mut self, ui: &mut Ui) {
ui.set_min_width(250.0);
CollapsingHeader::new("Widgets")
CollapsingHeader::new("Label")
.default_open(true)
.show(ui, |ui| {
label_ui(ui);
});
CollapsingHeader::new("Misc widgets")
.default_open(false)
.show(ui, |ui| {
self.widgets.ui(ui);
});
@@ -70,12 +71,12 @@ impl View for MiscDemoWindow {
CollapsingHeader::new("Text layout")
.default_open(false)
.show(ui, |ui| {
text_layout_ui(
ui,
&mut self.max_rows,
&mut self.break_anywhere,
&mut self.overflow_character,
);
text_layout_demo(ui);
ui.separator();
self.text_break.ui(ui);
ui.vertical_centered(|ui| {
ui.add(crate::egui_github_link_file_line!());
});
});
CollapsingHeader::new("Colors")
@@ -177,6 +178,43 @@ impl View for MiscDemoWindow {
// ----------------------------------------------------------------------------
fn label_ui(ui: &mut egui::Ui) {
ui.vertical_centered(|ui| {
ui.add(crate::egui_github_link_file_line!());
});
ui.horizontal_wrapped(|ui| {
// Trick so we don't have to add spaces in the text below:
let width = ui.fonts(|f|f.glyph_width(&TextStyle::Body.resolve(ui.style()), ' '));
ui.spacing_mut().item_spacing.x = width;
ui.label(RichText::new("Text can have").color(Color32::from_rgb(110, 255, 110)));
ui.colored_label(Color32::from_rgb(128, 140, 255), "color"); // Shortcut version
ui.label("and tooltips.").on_hover_text(
"This is a multiline tooltip that demonstrates that you can easily add tooltips to any element.\nThis is the second line.\nThis is the third.",
);
ui.label("You can mix in other widgets into text, like");
let _ = ui.small_button("this button");
ui.label(".");
ui.label("The default font supports all latin and cyrillic characters (ИÅđ…), common math symbols (∫√∞²⅓…), and many emojis (💓🌟🖩…).")
.on_hover_text("There is currently no support for right-to-left languages.");
ui.label("See the 🔤 Font Book for more!");
ui.monospace("There is also a monospace font.");
});
ui.add(
egui::Label::new(
"Labels containing long text can be set to elide the text that doesn't fit on a single line using `Label::elide`. When hovered, the label will show the full text.",
)
.truncate(true),
);
}
// ----------------------------------------------------------------------------
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct Widgets {
@@ -200,28 +238,6 @@ impl Widgets {
ui.add(crate::egui_github_link_file_line!());
});
ui.horizontal_wrapped(|ui| {
// Trick so we don't have to add spaces in the text below:
let width = ui.fonts(|f|f.glyph_width(&TextStyle::Body.resolve(ui.style()), ' '));
ui.spacing_mut().item_spacing.x = width;
ui.label(RichText::new("Text can have").color(Color32::from_rgb(110, 255, 110)));
ui.colored_label(Color32::from_rgb(128, 140, 255), "color"); // Shortcut version
ui.label("and tooltips.").on_hover_text(
"This is a multiline tooltip that demonstrates that you can easily add tooltips to any element.\nThis is the second line.\nThis is the third.",
);
ui.label("You can mix in other widgets into text, like");
let _ = ui.small_button("this button");
ui.label(".");
ui.label("The default font supports all latin and cyrillic characters (ИÅđ…), common math symbols (∫√∞²⅓…), and many emojis (💓🌟🖩…).")
.on_hover_text("There is currently no support for right-to-left languages.");
ui.label("See the 🔤 Font Book for more!");
ui.monospace("There is also a monospace font.");
});
let tooltip_ui = |ui: &mut Ui| {
ui.heading("The name of the tooltip");
ui.horizontal(|ui| {
@@ -455,7 +471,7 @@ impl Tree {
.into_iter()
.enumerate()
.filter_map(|(i, mut tree)| {
if tree.ui_impl(ui, depth + 1, &format!("child #{}", i)) == Action::Keep {
if tree.ui_impl(ui, depth + 1, &format!("child #{i}")) == Action::Keep {
Some(tree)
} else {
None
@@ -473,12 +489,7 @@ impl Tree {
// ----------------------------------------------------------------------------
fn text_layout_ui(
ui: &mut egui::Ui,
max_rows: &mut usize,
break_anywhere: &mut bool,
overflow_character: &mut Option<char>,
) {
fn text_layout_demo(ui: &mut Ui) {
use egui::text::LayoutJob;
let mut job = LayoutJob::default();
@@ -632,32 +643,64 @@ fn text_layout_ui(
);
ui.label(job);
ui.separator();
ui.horizontal(|ui| {
ui.add(DragValue::new(max_rows));
ui.label("Max rows");
});
ui.checkbox(break_anywhere, "Break anywhere");
ui.horizontal(|ui| {
ui.selectable_value(overflow_character, None, "None");
ui.selectable_value(overflow_character, Some('…'), "");
ui.selectable_value(overflow_character, Some('—'), "");
ui.selectable_value(overflow_character, Some('-'), " - ");
ui.label("Overflow character");
});
let mut job = LayoutJob::single_section(LOREM_IPSUM.to_owned(), TextFormat::default());
job.wrap = TextWrapping {
max_rows: *max_rows,
break_anywhere: *break_anywhere,
overflow_character: *overflow_character,
..Default::default()
};
ui.label(job);
ui.vertical_centered(|ui| {
ui.add(crate::egui_github_link_file_line!());
});
}
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
struct TextBreakDemo {
break_anywhere: bool,
max_rows: usize,
overflow_character: Option<char>,
}
impl Default for TextBreakDemo {
fn default() -> Self {
Self {
max_rows: 1,
break_anywhere: true,
overflow_character: Some('…'),
}
}
}
impl TextBreakDemo {
pub fn ui(&mut self, ui: &mut Ui) {
let Self {
break_anywhere,
max_rows,
overflow_character,
} = self;
use egui::text::LayoutJob;
ui.horizontal(|ui| {
ui.add(DragValue::new(max_rows));
ui.label("Max rows");
});
ui.horizontal(|ui| {
ui.label("Line-break:");
ui.radio_value(break_anywhere, false, "word boundaries");
ui.radio_value(break_anywhere, true, "anywhere");
});
ui.horizontal(|ui| {
ui.selectable_value(overflow_character, None, "None");
ui.selectable_value(overflow_character, Some('…'), "");
ui.selectable_value(overflow_character, Some('—'), "");
ui.selectable_value(overflow_character, Some('-'), " - ");
ui.label("Overflow character");
});
let mut job =
LayoutJob::single_section(crate::LOREM_IPSUM_LONG.to_owned(), TextFormat::default());
job.wrap = TextWrapping {
max_rows: *max_rows,
break_anywhere: *break_anywhere,
overflow_character: *overflow_character,
..Default::default()
};
ui.label(job); // `Label` overrides some of the wrapping settings, e.g. wrap width
}
}

View File

@@ -50,7 +50,7 @@ impl super::View for MultiTouch {
ui.label("Try touch gestures Pinch/Stretch, Rotation, and Pressure with 2+ fingers.");
let num_touches = ui.input(|i| i.multi_touch().map_or(0, |mt| mt.num_touches));
ui.label(format!("Current touches: {}", num_touches));
ui.label(format!("Current touches: {num_touches}"));
let color = if ui.visuals().dark_mode {
Color32::WHITE

View File

@@ -39,8 +39,8 @@ pub struct PlotDemo {
charts_demo: ChartsDemo,
items_demo: ItemsDemo,
interaction_demo: InteractionDemo,
custom_axes_demo: CustomAxisDemo,
linked_axes_demo: LinkedAxisDemo,
custom_axes_demo: CustomAxesDemo,
linked_axes_demo: LinkedAxesDemo,
open_panel: Panel,
}
@@ -322,7 +322,7 @@ impl MarkerDemo {
[5.0, 0.0 + y_offset],
[6.0, 0.5 + y_offset],
])
.name(format!("{:?}", marker))
.name(format!("{marker:?}"))
.filled(self.fill_markers)
.radius(self.marker_radius)
.shape(marker);
@@ -412,7 +412,7 @@ impl LegendDemo {
ui.label("Position:");
ui.horizontal(|ui| {
Corner::all().for_each(|position| {
ui.selectable_value(&mut config.position, position, format!("{:?}", position));
ui.selectable_value(&mut config.position, position, format!("{position:?}"));
});
});
ui.end_row();
@@ -446,19 +446,19 @@ impl LegendDemo {
// ----------------------------------------------------------------------------
#[derive(PartialEq, Default)]
struct CustomAxisDemo {}
struct CustomAxesDemo {}
impl CustomAxisDemo {
impl CustomAxesDemo {
const MINS_PER_DAY: f64 = 24.0 * 60.0;
const MINS_PER_H: f64 = 60.0;
fn logistic_fn() -> Line {
fn days(min: f64) -> f64 {
CustomAxisDemo::MINS_PER_DAY * min
CustomAxesDemo::MINS_PER_DAY * min
}
let values = PlotPoints::from_explicit_callback(
move |x| 1.0 / (1.0 + (-2.5 * (x / CustomAxisDemo::MINS_PER_DAY - 2.0)).exp()),
move |x| 1.0 / (1.0 + (-2.5 * (x / CustomAxesDemo::MINS_PER_DAY - 2.0)).exp()),
days(0.0)..days(5.0),
100,
);
@@ -502,8 +502,8 @@ impl CustomAxisDemo {
#[allow(clippy::unused_self)]
fn ui(&mut self, ui: &mut Ui) -> Response {
const MINS_PER_DAY: f64 = CustomAxisDemo::MINS_PER_DAY;
const MINS_PER_H: f64 = CustomAxisDemo::MINS_PER_H;
const MINS_PER_DAY: f64 = CustomAxesDemo::MINS_PER_DAY;
const MINS_PER_H: f64 = CustomAxesDemo::MINS_PER_H;
fn day(x: f64) -> f64 {
(x / MINS_PER_DAY).floor()
@@ -572,10 +572,10 @@ impl CustomAxisDemo {
.data_aspect(2.0 * MINS_PER_DAY as f32)
.custom_x_axes(x_axes)
.custom_y_axes(y_axes)
.x_grid_spacer(CustomAxisDemo::x_grid)
.x_grid_spacer(CustomAxesDemo::x_grid)
.label_formatter(label_fmt)
.show(ui, |plot_ui| {
plot_ui.line(CustomAxisDemo::logistic_fn());
plot_ui.line(CustomAxesDemo::logistic_fn());
})
.response
}
@@ -584,14 +584,14 @@ impl CustomAxisDemo {
// ----------------------------------------------------------------------------
#[derive(PartialEq)]
struct LinkedAxisDemo {
struct LinkedAxesDemo {
link_x: bool,
link_y: bool,
link_cursor_x: bool,
link_cursor_y: bool,
}
impl Default for LinkedAxisDemo {
impl Default for LinkedAxesDemo {
fn default() -> Self {
let link_x = true;
let link_y = false;
@@ -606,7 +606,7 @@ impl Default for LinkedAxisDemo {
}
}
impl LinkedAxisDemo {
impl LinkedAxesDemo {
fn line_with_slope(slope: f64) -> Line {
Line::new(PlotPoints::from_explicit_callback(
move |x| slope * x,
@@ -632,11 +632,11 @@ impl LinkedAxisDemo {
}
fn configure_plot(plot_ui: &mut plot::PlotUi) {
plot_ui.line(LinkedAxisDemo::line_with_slope(0.5));
plot_ui.line(LinkedAxisDemo::line_with_slope(1.0));
plot_ui.line(LinkedAxisDemo::line_with_slope(2.0));
plot_ui.line(LinkedAxisDemo::sin());
plot_ui.line(LinkedAxisDemo::cos());
plot_ui.line(LinkedAxesDemo::line_with_slope(0.5));
plot_ui.line(LinkedAxesDemo::line_with_slope(1.0));
plot_ui.line(LinkedAxesDemo::line_with_slope(2.0));
plot_ui.line(LinkedAxesDemo::sin());
plot_ui.line(LinkedAxesDemo::cos());
}
fn ui(&mut self, ui: &mut Ui) -> Response {
@@ -659,7 +659,7 @@ impl LinkedAxisDemo {
.height(250.0)
.link_axis(link_group_id, self.link_x, self.link_y)
.link_cursor(link_group_id, self.link_cursor_x, self.link_cursor_y)
.show(ui, LinkedAxisDemo::configure_plot);
.show(ui, LinkedAxesDemo::configure_plot);
Plot::new("linked_axis_2")
.data_aspect(2.0)
.width(150.0)
@@ -669,7 +669,7 @@ impl LinkedAxisDemo {
.y_axis_width(3)
.link_axis(link_group_id, self.link_x, self.link_y)
.link_cursor(link_group_id, self.link_cursor_x, self.link_cursor_y)
.show(ui, LinkedAxisDemo::configure_plot);
.show(ui, LinkedAxesDemo::configure_plot);
});
Plot::new("linked_axis_3")
.data_aspect(0.5)
@@ -678,7 +678,7 @@ impl LinkedAxisDemo {
.x_axis_label("x")
.link_axis(link_group_id, self.link_x, self.link_y)
.link_cursor(link_group_id, self.link_cursor_x, self.link_cursor_y)
.show(ui, LinkedAxisDemo::configure_plot)
.show(ui, LinkedAxesDemo::configure_plot)
.response
}
}
@@ -776,7 +776,7 @@ impl InteractionDemo {
plot_ui.pointer_coordinate(),
plot_ui.pointer_coordinate_drag_delta(),
plot_ui.plot_bounds(),
plot_ui.plot_hovered(),
plot_ui.response().hovered(),
)
});
@@ -789,21 +789,18 @@ impl InteractionDemo {
"origin in screen coordinates: x: {:.02}, y: {:.02}",
screen_pos.x, screen_pos.y
));
ui.label(format!("plot hovered: {}", hovered));
ui.label(format!("plot hovered: {hovered}"));
let coordinate_text = if let Some(coordinate) = pointer_coordinate {
format!("x: {:.02}, y: {:.02}", coordinate.x, coordinate.y)
} else {
"None".to_owned()
};
ui.label(format!("pointer coordinate: {}", coordinate_text));
ui.label(format!("pointer coordinate: {coordinate_text}"));
let coordinate_text = format!(
"x: {:.02}, y: {:.02}",
pointer_coordinate_drag_delta.x, pointer_coordinate_drag_delta.y
);
ui.label(format!(
"pointer coordinate drag delta: {}",
coordinate_text
));
ui.label(format!("pointer coordinate drag delta: {coordinate_text}"));
response
}

View File

@@ -232,10 +232,10 @@ impl super::View for ScrollTo {
for item in 1..=50 {
if track_item && item == self.track_item {
let response =
ui.colored_label(Color32::YELLOW, format!("This is item {}", item));
ui.colored_label(Color32::YELLOW, format!("This is item {item}"));
response.scroll_to_me(self.tack_item_align);
} else {
ui.label(format!("This is item {}", item));
ui.label(format!("This is item {item}"));
}
}
});
@@ -254,8 +254,7 @@ impl super::View for ScrollTo {
ui.separator();
ui.label(format!(
"Scroll offset: {:.0}/{:.0} px",
current_scroll, max_scroll
"Scroll offset: {current_scroll:.0}/{max_scroll:.0} px"
));
ui.separator();

View File

@@ -37,7 +37,7 @@ impl super::View for StripDemo {
.size(Size::exact(50.0))
.size(Size::remainder())
.size(Size::relative(0.5).at_least(60.0))
.size(Size::exact(10.0))
.size(Size::exact(10.5))
.vertical(|mut strip| {
strip.cell(|ui| {
ui.painter().rect_filled(

View File

@@ -38,7 +38,6 @@ impl super::Demo for TableDemo {
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
egui::Window::new(self.name())
.open(open)
.resizable(true)
.default_width(400.0)
.show(ctx, |ui| {
use super::View as _;
@@ -102,7 +101,7 @@ impl super::View for TableDemo {
use egui_extras::{Size, StripBuilder};
StripBuilder::new(ui)
.size(Size::remainder().at_least(100.0)) // for the table
.size(Size::exact(10.0)) // for the source code link
.size(Size::exact(10.5)) // for the source code link
.vertical(|mut strip| {
strip.cell(|ui| {
egui::ScrollArea::horizontal().show(ui, |ui| {

View File

@@ -20,7 +20,7 @@ impl super::View for CursorTest {
ui.heading("Hover to switch cursor icon:");
for &cursor_icon in &egui::CursorIcon::ALL {
let _ = ui
.button(format!("{:?}", cursor_icon))
.button(format!("{cursor_icon:?}"))
.on_hover_cursor(cursor_icon);
}
ui.add(crate::egui_github_link_file!());
@@ -239,7 +239,7 @@ impl super::View for TableTest {
for row in 0..self.num_rows {
for col in 0..self.num_cols {
if col == 0 {
ui.label(format!("row {}", row));
ui.label(format!("row {row}"));
} else {
let word_idx = row * 3 + col * 5;
let word_count = (row * 5 + col * 75) % 13;
@@ -350,13 +350,13 @@ impl super::View for InputTest {
use std::fmt::Write as _;
if response.clicked_by(button) {
writeln!(new_info, "Clicked by {:?} button", button).ok();
writeln!(new_info, "Clicked by {button:?} button").ok();
}
if response.double_clicked_by(button) {
writeln!(new_info, "Double-clicked by {:?} button", button).ok();
writeln!(new_info, "Double-clicked by {button:?} button").ok();
}
if response.triple_clicked_by(button) {
writeln!(new_info, "Triple-clicked by {:?} button", button).ok();
writeln!(new_info, "Triple-clicked by {button:?} button").ok();
}
if response.dragged_by(button) {
writeln!(

View File

@@ -126,7 +126,7 @@ impl WidgetGallery {
ui.add(doc_link_label("Hyperlink", "Hyperlink"));
use egui::special_emojis::GITHUB;
ui.hyperlink_to(
format!("{} egui on GitHub", GITHUB),
format!("{GITHUB} egui on GitHub"),
"https://github.com/emilk/egui",
);
ui.end_row();
@@ -173,7 +173,7 @@ impl WidgetGallery {
ui.add(doc_link_label("ComboBox", "ComboBox"));
egui::ComboBox::from_label("Take your pick")
.selected_text(format!("{:?}", radio))
.selected_text(format!("{radio:?}"))
.show_ui(ui, |ui| {
ui.style_mut().wrap = Some(false);
ui.set_min_width(60.0);
@@ -278,8 +278,8 @@ fn example_plot(ui: &mut egui::Ui) -> egui::Response {
}
fn doc_link_label<'a>(title: &'a str, search_term: &'a str) -> impl egui::Widget + 'a {
let label = format!("{}:", title);
let url = format!("https://docs.rs/egui?search={}", search_term);
let label = format!("{title}:");
let url = format!("https://docs.rs/egui?search={search_term}");
move |ui: &mut egui::Ui| {
ui.hyperlink_to(label, url).on_hover_ui(|ui| {
ui.horizontal_wrapped(|ui| {

View File

@@ -161,7 +161,7 @@ fn numbered_point(ui: &mut Ui, width: f32, number: &str) -> Response {
let font_id = TextStyle::Body.resolve(ui.style());
let row_height = ui.fonts(|f| f.row_height(&font_id));
let (rect, response) = ui.allocate_exact_size(vec2(width, row_height), Sense::hover());
let text = format!("{}.", number);
let text = format!("{number}.");
let text_color = ui.visuals().strong_text_color();
ui.painter().text(
rect.right_center(),

View File

@@ -1,8 +1,8 @@
# Changelog for egui_extras
All notable changes to the `egui_extras` integration will be noted in this file.
## Unreleased
This file is updated upon each release.
Changes since the last release can be found by running the `scripts/generate_changelog.py` script.
## 0.22.0 - 2023-05-23

View File

@@ -8,7 +8,7 @@ authors = [
]
description = "Extra functionality and widgets for the egui GUI library"
edition = "2021"
rust-version = "1.65"
rust-version = "1.67"
homepage = "https://github.com/emilk/egui"
license = "MIT OR Apache-2.0"
readme = "README.md"

View File

@@ -61,7 +61,7 @@ impl<'a> DatePickerButton<'a> {
self
}
/// Show the calender icon on the button. (Default: true)
/// Show the calendar icon on the button. (Default: true)
pub fn show_icon(mut self, show_icon: bool) -> Self {
self.show_icon = show_icon;
self

View File

@@ -428,6 +428,6 @@ fn month_name(i: u32) -> &'static str {
10 => "October",
11 => "November",
12 => "December",
_ => panic!("Unknown month: {}", i),
_ => panic!("Unknown month: {i}"),
}
}

View File

@@ -10,11 +10,15 @@ pub use usvg::FitTo;
/// Use the `svg` and `image` features to enable more constructors.
pub struct RetainedImage {
debug_name: String,
size: [usize; 2],
/// Cleared once [`Self::texture`] has been loaded.
image: Mutex<egui::ColorImage>,
/// Lazily loaded when we have an egui context.
texture: Mutex<Option<egui::TextureHandle>>,
options: TextureOptions,
}
@@ -254,7 +258,7 @@ pub fn load_svg_bytes_with_size(
};
let mut pixmap = tiny_skia::Pixmap::new(w, h)
.ok_or_else(|| format!("Failed to create SVG Pixmap of size {}x{}", w, h))?;
.ok_or_else(|| format!("Failed to create SVG Pixmap of size {w}x{h}"))?;
resvg::render(&rtree, fit_to, Default::default(), pixmap.as_mut())
.ok_or_else(|| "Failed to render SVG".to_owned())?;

View File

@@ -32,9 +32,11 @@ pub struct StripLayout<'l> {
direction: CellDirection,
pub(crate) rect: Rect,
pub(crate) cursor: Pos2,
/// Keeps track of the max used position,
/// so we know how much space we used.
max: Pos2,
cell_layout: egui::Layout,
}

View File

@@ -1,14 +1,16 @@
use egui::Rangef;
/// Size hint for table column/strip cell.
#[derive(Clone, Debug, Copy)]
pub enum Size {
/// Absolute size in points, with a given range of allowed sizes to resize within.
Absolute { initial: f32, range: (f32, f32) },
Absolute { initial: f32, range: Rangef },
/// Relative size relative to all available space.
Relative { fraction: f32, range: (f32, f32) },
Relative { fraction: f32, range: Rangef },
/// Multiple remainders each get the same space.
Remainder { range: (f32, f32) },
Remainder { range: Rangef },
}
impl Size {
@@ -16,7 +18,7 @@ impl Size {
pub fn exact(points: f32) -> Self {
Self::Absolute {
initial: points,
range: (points, points),
range: Rangef::new(points, points),
}
}
@@ -24,7 +26,7 @@ impl Size {
pub fn initial(points: f32) -> Self {
Self::Absolute {
initial: points,
range: (0.0, f32::INFINITY),
range: Rangef::new(0.0, f32::INFINITY),
}
}
@@ -33,14 +35,14 @@ impl Size {
egui::egui_assert!(0.0 <= fraction && fraction <= 1.0);
Self::Relative {
fraction,
range: (0.0, f32::INFINITY),
range: Rangef::new(0.0, f32::INFINITY),
}
}
/// Multiple remainders each get the same space.
pub fn remainder() -> Self {
Self::Remainder {
range: (0.0, f32::INFINITY),
range: Rangef::new(0.0, f32::INFINITY),
}
}
@@ -50,7 +52,7 @@ impl Size {
Self::Absolute { range, .. }
| Self::Relative { range, .. }
| Self::Remainder { range, .. } => {
range.0 = minimum;
range.min = minimum;
}
}
self
@@ -62,14 +64,14 @@ impl Size {
Self::Absolute { range, .. }
| Self::Relative { range, .. }
| Self::Remainder { range, .. } => {
range.1 = maximum;
range.max = maximum;
}
}
self
}
/// Allowed range of movement (in points), if in a resizable [`Table`](crate::table::Table).
pub fn range(self) -> (f32, f32) {
pub fn range(self) -> Rangef {
match self {
Self::Absolute { range, .. }
| Self::Relative { range, .. }
@@ -99,12 +101,9 @@ impl Sizing {
.iter()
.map(|&size| match size {
Size::Absolute { initial, .. } => initial,
Size::Relative {
fraction,
range: (min, max),
} => {
Size::Relative { fraction, range } => {
assert!(0.0 <= fraction && fraction <= 1.0);
(length * fraction).clamp(min, max)
range.clamp(length * fraction)
}
Size::Remainder { .. } => {
remainders += 1;
@@ -120,9 +119,9 @@ impl Sizing {
let mut remainder_length = length - sum_non_remainder;
let avg_remainder_length = 0.0f32.max(remainder_length / remainders as f32).floor();
self.sizes.iter().for_each(|&size| {
if let Size::Remainder { range: (min, _max) } = size {
if avg_remainder_length < min {
remainder_length -= min;
if let Size::Remainder { range } = size {
if avg_remainder_length < range.min {
remainder_length -= range.min;
remainders -= 1;
}
}
@@ -138,11 +137,8 @@ impl Sizing {
.iter()
.map(|&size| match size {
Size::Absolute { initial, .. } => initial,
Size::Relative {
fraction,
range: (min, max),
} => (length * fraction).clamp(min, max),
Size::Remainder { range: (min, max) } => avg_remainder_length.clamp(min, max),
Size::Relative { fraction, range } => range.clamp(length * fraction),
Size::Remainder { range } => range.clamp(avg_remainder_length),
})
.collect()
}

View File

@@ -72,13 +72,13 @@ impl<'a> StripBuilder<'a> {
self
}
/// Allocate space for for one column/row.
/// Allocate space for one column/row.
pub fn size(mut self, size: Size) -> Self {
self.sizing.add(size);
self
}
/// Allocate space for for several columns/rows at once.
/// Allocate space for several columns/rows at once.
pub fn sizes(mut self, size: Size, count: usize) -> Self {
for _ in 0..count {
self.sizing.add(size);

View File

@@ -3,7 +3,7 @@
//! | fixed size | all available space/minimum | 30% of available width | fixed size |
//! Takes all available height, so if you want something below the table, put it in a strip.
use egui::{Align, NumExt as _, Rect, Response, ScrollArea, Ui, Vec2};
use egui::{Align, NumExt as _, Rangef, Rect, Response, ScrollArea, Ui, Vec2};
use crate::{
layout::{CellDirection, CellSize},
@@ -28,7 +28,9 @@ enum InitialColumnSize {
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Column {
initial_width: InitialColumnSize,
width_range: (f32, f32),
width_range: Rangef,
/// Clip contents if too narrow?
clip: bool,
@@ -78,7 +80,7 @@ impl Column {
fn new(initial_width: InitialColumnSize) -> Self {
Self {
initial_width,
width_range: (0.0, f32::INFINITY),
width_range: Rangef::new(0.0, f32::INFINITY),
resizable: None,
clip: false,
}
@@ -110,7 +112,7 @@ impl Column {
///
/// Default: 0.0
pub fn at_least(mut self, minimum: f32) -> Self {
self.width_range.0 = minimum;
self.width_range.min = minimum;
self
}
@@ -118,13 +120,13 @@ impl Column {
///
/// Default: [`f32::INFINITY`]
pub fn at_most(mut self, maximum: f32) -> Self {
self.width_range.1 = maximum;
self.width_range.max = maximum;
self
}
/// Allowed range of movement (in points), if in a resizable [`Table`](crate::table::Table).
pub fn range(mut self, range: std::ops::RangeInclusive<f32>) -> Self {
self.width_range = (*range.start(), *range.end());
pub fn range(mut self, range: impl Into<Rangef>) -> Self {
self.width_range = range.into();
self
}
@@ -146,8 +148,8 @@ fn to_sizing(columns: &[Column]) -> crate::sizing::Sizing {
InitialColumnSize::Automatic(suggested_width) => Size::initial(suggested_width),
InitialColumnSize::Remainder => Size::remainder(),
}
.at_least(column.width_range.0)
.at_most(column.width_range.1);
.at_least(column.width_range.min)
.at_most(column.width_range.max);
sizing.add(size);
}
sizing
@@ -511,8 +513,10 @@ pub struct Table<'a> {
columns: Vec<Column>,
available_width: f32,
state: TableState,
/// Accumulated maximum used widths for each column.
max_used_widths: Vec<f32>,
first_frame_auto_size_columns: bool,
resizable: bool,
striped: bool,
@@ -598,13 +602,13 @@ impl<'a> Table<'a> {
if scroll_to_row.is_some() && scroll_to_y_range.is_none() {
// TableBody::row didn't find the right row, so scroll to the bottom:
scroll_to_y_range = Some((f32::INFINITY, f32::INFINITY));
scroll_to_y_range = Some(Rangef::new(f32::INFINITY, f32::INFINITY));
}
});
if let Some((min_y, max_y)) = scroll_to_y_range {
if let Some(y_range) = scroll_to_y_range {
let x = 0.0; // ignored, we only have vertical scrolling
let rect = egui::Rect::from_min_max(egui::pos2(x, min_y), egui::pos2(x, max_y));
let rect = egui::Rect::from_x_y_ranges(x..=x, y_range);
let align = scroll_to_row.and_then(|(_, a)| a);
ui.scroll_to_rect(rect, align);
}
@@ -617,14 +621,14 @@ impl<'a> Table<'a> {
for (i, column_width) in state.column_widths.iter_mut().enumerate() {
let column = &columns[i];
let column_is_resizable = column.resizable.unwrap_or(resizable);
let (min_width, max_width) = column.width_range;
let width_range = column.width_range;
if !column.clip {
// Unless we clip we don't want to shrink below the
// size that was actually used:
*column_width = column_width.at_least(max_used_widths[i]);
}
*column_width = column_width.clamp(min_width, max_width);
*column_width = width_range.clamp(*column_width);
let is_last_column = i + 1 == columns.len();
@@ -633,7 +637,7 @@ impl<'a> Table<'a> {
let eps = 0.1; // just to avoid some rounding errors.
*column_width = available_width - eps;
*column_width = column_width.at_least(max_used_widths[i]);
*column_width = column_width.clamp(min_width, max_width);
*column_width = width_range.clamp(*column_width);
break;
}
@@ -641,7 +645,7 @@ impl<'a> Table<'a> {
if column.is_auto() && (first_frame_auto_size_columns || !column_is_resizable) {
*column_width = max_used_widths[i];
*column_width = column_width.clamp(min_width, max_width);
*column_width = width_range.clamp(*column_width);
} else if column_is_resizable {
let column_resize_id = ui.id().with("resize_column").with(i);
@@ -656,7 +660,7 @@ impl<'a> Table<'a> {
if resize_response.double_clicked() {
// Resize to the minimum of what is needed.
*column_width = max_used_widths[i].clamp(min_width, max_width);
*column_width = width_range.clamp(max_used_widths[i]);
} else if resize_response.dragged() {
if let Some(pointer) = ui.ctx().pointer_latest_pos() {
let mut new_width = *column_width + pointer.x - x;
@@ -671,7 +675,7 @@ impl<'a> Table<'a> {
new_width =
new_width.at_least(max_used_widths[i] - max_shrinkage_per_frame);
}
new_width = new_width.clamp(min_width, max_width);
new_width = width_range.clamp(new_width);
let x = x - *column_width + new_width;
(p0.x, p1.x) = (x, x);
@@ -731,7 +735,7 @@ pub struct TableBody<'a> {
/// If we find the correct row to scroll to,
/// this is set to the y-range of the row.
scroll_to_y_range: &'a mut Option<(f32, f32)>,
scroll_to_y_range: &'a mut Option<Rangef>,
}
impl<'a> TableBody<'a> {
@@ -779,7 +783,7 @@ impl<'a> TableBody<'a> {
let bottom_y = self.layout.cursor.y;
if Some(self.row_nr) == self.scroll_to_row {
*self.scroll_to_y_range = Some((top_y, bottom_y));
*self.scroll_to_y_range = Some(Rangef::new(top_y, bottom_y));
}
self.row_nr += 1;
@@ -819,7 +823,7 @@ impl<'a> TableBody<'a> {
if let Some(scroll_to_row) = self.scroll_to_row {
let scroll_to_row = scroll_to_row.at_most(total_rows.saturating_sub(1)) as f32;
*self.scroll_to_y_range = Some((
*self.scroll_to_y_range = Some(Rangef::new(
self.layout.cursor.y + scroll_to_row * row_height_with_spacing,
self.layout.cursor.y + (scroll_to_row + 1.0) * row_height_with_spacing,
));
@@ -909,7 +913,7 @@ impl<'a> TableBody<'a> {
cursor_y += (row_height + spacing.y) as f64;
if Some(row_index) == self.scroll_to_row {
*self.scroll_to_y_range = Some((
*self.scroll_to_y_range = Some(Rangef::new(
(scroll_to_y_range_offset + old_cursor_y) as f32,
(scroll_to_y_range_offset + cursor_y) as f32,
));
@@ -953,7 +957,7 @@ impl<'a> TableBody<'a> {
cursor_y += (row_height + spacing.y) as f64;
if Some(row_index) == self.scroll_to_row {
*self.scroll_to_y_range = Some((
*self.scroll_to_y_range = Some(Rangef::new(
(scroll_to_y_range_offset + top_y) as f32,
(scroll_to_y_range_offset + cursor_y) as f32,
));
@@ -972,7 +976,7 @@ impl<'a> TableBody<'a> {
let top_y = cursor_y;
cursor_y += (row_height + spacing.y) as f64;
if Some(row_index) == self.scroll_to_row {
*self.scroll_to_y_range = Some((
*self.scroll_to_y_range = Some(Rangef::new(
(scroll_to_y_range_offset + top_y) as f32,
(scroll_to_y_range_offset + cursor_y) as f32,
));
@@ -981,10 +985,8 @@ impl<'a> TableBody<'a> {
if self.scroll_to_row.is_some() && self.scroll_to_y_range.is_none() {
// Catch desire to scroll past the end:
*self.scroll_to_y_range = Some((
(scroll_to_y_range_offset + cursor_y) as f32,
(scroll_to_y_range_offset + cursor_y) as f32,
));
*self.scroll_to_y_range =
Some(Rangef::point((scroll_to_y_range_offset + cursor_y) as f32));
}
if height_below_visible > 0.0 {
@@ -1013,8 +1015,10 @@ pub struct TableRow<'a, 'b> {
layout: &'b mut StripLayout<'a>,
columns: &'b [Column],
widths: &'b [f32],
/// grows during building with the maximum widths
max_used_widths: &'b mut [f32],
col_index: usize,
striped: bool,
height: f32,

View File

@@ -1,6 +1,9 @@
# Changelog for egui_glium
All notable changes to the `egui_glium` integration will be noted in this file.
This file is updated upon each release.
Changes since the last release can be found by running the `scripts/generate_changelog.py` script.
## Unreleased
* Remove the `screen_reader` feature ([#2669](https://github.com/emilk/egui/pull/2669)).

View File

@@ -4,7 +4,7 @@ version = "0.22.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "Bindings for using egui natively using the glium library"
edition = "2021"
rust-version = "1.65"
rust-version = "1.67"
homepage = "https://github.com/emilk/egui/tree/master/crates/egui_glium"
license = "MIT OR Apache-2.0"
readme = "README.md"

View File

@@ -78,8 +78,8 @@ fn main() {
// Platform-dependent event handlers to workaround a winit bug
// See: https://github.com/rust-windowing/winit/issues/987
// See: https://github.com/rust-windowing/winit/issues/1619
glutin::event::Event::RedrawEventsCleared if cfg!(windows) => redraw(),
glutin::event::Event::RedrawRequested(_) if !cfg!(windows) => redraw(),
glutin::event::Event::RedrawEventsCleared if cfg!(target_os = "windows") => redraw(),
glutin::event::Event::RedrawRequested(_) if !cfg!(target_os = "windows") => redraw(),
glutin::event::Event::WindowEvent { event, .. } => {
use glutin::event::WindowEvent;

View File

@@ -65,8 +65,8 @@ fn main() {
// Platform-dependent event handlers to workaround a winit bug
// See: https://github.com/rust-windowing/winit/issues/987
// See: https://github.com/rust-windowing/winit/issues/1619
glutin::event::Event::RedrawEventsCleared if cfg!(windows) => redraw(),
glutin::event::Event::RedrawRequested(_) if !cfg!(windows) => redraw(),
glutin::event::Event::RedrawEventsCleared if cfg!(target_os = "windows") => redraw(),
glutin::event::Event::RedrawRequested(_) if !cfg!(target_os = "windows") => redraw(),
glutin::event::Event::WindowEvent { event, .. } => {
use glutin::event::WindowEvent;

View File

@@ -1,8 +1,8 @@
# Changelog for egui_glow
All notable changes to the `egui_glow` integration will be noted in this file.
## Unreleased
This file is updated upon each release.
Changes since the last release can be found by running the `scripts/generate_changelog.py` script.
## 0.22.0 - 2023-05-23

View File

@@ -4,7 +4,7 @@ version = "0.22.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "Bindings for using egui natively using the glow library"
edition = "2021"
rust-version = "1.65"
rust-version = "1.67"
homepage = "https://github.com/emilk/egui/tree/master/crates/egui_glow"
license = "MIT OR Apache-2.0"
readme = "README.md"
@@ -60,7 +60,7 @@ document-features = { version = "0.2", optional = true }
# Native:
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
egui-winit = { version = "0.22.0", path = "../egui-winit", optional = true, default-features = false }
puffin = { version = "0.15", optional = true }
puffin = { version = "0.16", optional = true }
# Web:
[target.'cfg(target_arch = "wasm32")'.dependencies]

View File

@@ -200,8 +200,8 @@ fn main() {
// Platform-dependent event handlers to workaround a winit bug
// See: https://github.com/rust-windowing/winit/issues/987
// See: https://github.com/rust-windowing/winit/issues/1619
winit::event::Event::RedrawEventsCleared if cfg!(windows) => redraw(),
winit::event::Event::RedrawRequested(_) if !cfg!(windows) => redraw(),
winit::event::Event::RedrawEventsCleared if cfg!(target_os = "windows") => redraw(),
winit::event::Event::RedrawRequested(_) if !cfg!(target_os = "windows") => redraw(),
winit::event::Event::WindowEvent { event, .. } => {
use winit::event::WindowEvent;

View File

@@ -4,7 +4,7 @@ version = "0.22.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "Minimal 2D math library for GUI work"
edition = "2021"
rust-version = "1.65"
rust-version = "1.67"
homepage = "https://github.com/emilk/egui/tree/master/crates/emath"
license = "MIT OR Apache-2.0"
readme = "README.md"

View File

@@ -110,29 +110,25 @@ impl Align {
/// assert_eq!(Max .align_size_within_range(INFINITY, NEG_INFINITY..=20.0), NEG_INFINITY..=20.0);
/// ```
#[inline]
pub fn align_size_within_range(
self,
size: f32,
range: RangeInclusive<f32>,
) -> RangeInclusive<f32> {
let min = *range.start();
let max = *range.end();
pub fn align_size_within_range(self, size: f32, range: impl Into<Rangef>) -> Rangef {
let range = range.into();
let Rangef { min, max } = range;
if max - min == f32::INFINITY && size == f32::INFINITY {
return range;
}
match self {
Self::Min => min..=min + size,
Self::Min => Rangef::new(min, min + size),
Self::Center => {
if size == f32::INFINITY {
f32::NEG_INFINITY..=f32::INFINITY
Rangef::new(f32::NEG_INFINITY, f32::INFINITY)
} else {
let left = (min + max) / 2.0 - size / 2.0;
left..=left + size
Rangef::new(left, left + size)
}
}
Self::Max => max - size..=max,
Self::Max => Rangef::new(max - size, max),
}
}
}

View File

@@ -99,11 +99,12 @@ impl Real for f64 {}
/// assert_eq!(lerp(1.0..=5.0, 2.0), 9.0);
/// ```
#[inline(always)]
pub fn lerp<R, T>(range: RangeInclusive<R>, t: T) -> R
pub fn lerp<R, T>(range: impl Into<RangeInclusive<R>>, t: T) -> R
where
T: Real + Mul<R, Output = R>,
R: Copy + Add<R, Output = R>,
{
let range = range.into();
(T::one() - t) * *range.start() + t * *range.end()
}
@@ -138,20 +139,28 @@ where
/// Linearly remap a value from one range to another,
/// so that when `x == from.start()` returns `to.start()`
/// and when `x == from.end()` returns `to.end()`.
pub fn remap<T>(x: T, from: RangeInclusive<T>, to: RangeInclusive<T>) -> T
pub fn remap<T>(x: T, from: impl Into<RangeInclusive<T>>, to: impl Into<RangeInclusive<T>>) -> T
where
T: Real,
{
let from = from.into();
let to = to.into();
crate::emath_assert!(from.start() != from.end());
let t = (x - *from.start()) / (*from.end() - *from.start());
lerp(to, t)
}
/// Like [`remap`], but also clamps the value so that the returned value is always in the `to` range.
pub fn remap_clamp<T>(x: T, from: RangeInclusive<T>, to: RangeInclusive<T>) -> T
pub fn remap_clamp<T>(
x: T,
from: impl Into<RangeInclusive<T>>,
to: impl Into<RangeInclusive<T>>,
) -> T
where
T: Real,
{
let from = from.into();
let to = to.into();
if from.end() < from.start() {
return remap_clamp(x, *from.end()..=*from.start(), *to.end()..=*to.start());
}
@@ -174,9 +183,7 @@ where
/// Round a value to the given number of decimal places.
pub fn round_to_decimals(value: f64, decimal_places: usize) -> f64 {
// This is a stupid way of doing this, but stupid works.
format!("{:.*}", decimal_places, value)
.parse()
.unwrap_or(value)
format!("{value:.decimal_places$}").parse().unwrap_or(value)
}
pub fn format_with_minimum_decimals(value: f64, decimals: usize) -> String {
@@ -194,7 +201,7 @@ pub fn format_with_decimals_in_range(value: f64, decimal_range: RangeInclusive<u
if min_decimals != max_decimals {
// Ugly/slow way of doing this. TODO(emilk): clean up precision.
for decimals in min_decimals..max_decimals {
let text = format!("{:.*}", decimals, value);
let text = format!("{value:.decimals$}");
let epsilon = 16.0 * f32::EPSILON; // margin large enough to handle most peoples round-tripping needs
if almost_equal(text.parse::<f32>().unwrap(), value as f32, epsilon) {
// Enough precision to show the value accurately - good!
@@ -205,7 +212,7 @@ pub fn format_with_decimals_in_range(value: f64, decimal_range: RangeInclusive<u
// Probably the value was set not by the slider, but from outside.
// In any case: show the full value
}
format!("{:.*}", max_decimals, value)
format!("{value:.max_decimals$}")
}
/// Return true when arguments are the same within some rounding error.

View File

@@ -206,7 +206,7 @@ impl std::ops::Index<usize> for Pos2 {
match index {
0 => &self.x,
1 => &self.y,
_ => panic!("Pos2 index out of bounds: {}", index),
_ => panic!("Pos2 index out of bounds: {index}"),
}
}
}
@@ -217,7 +217,7 @@ impl std::ops::IndexMut<usize> for Pos2 {
match index {
0 => &mut self.x,
1 => &mut self.y,
_ => panic!("Pos2 index out of bounds: {}", index),
_ => panic!("Pos2 index out of bounds: {index}"),
}
}
}

View File

@@ -36,14 +36,60 @@ impl Rangef {
}
#[inline]
pub fn span(&self) -> f32 {
pub fn point(min_and_max: f32) -> Self {
Self {
min: min_and_max,
max: min_and_max,
}
}
/// The length of the range, i.e. `max - min`.
#[inline]
pub fn span(self) -> f32 {
self.max - self.min
}
#[inline]
pub fn contains(&self, x: f32) -> bool {
#[must_use]
pub fn contains(self, x: f32) -> bool {
self.min <= x && x <= self.max
}
/// Equivalent to `x.clamp(min, max)`
#[inline]
#[must_use]
pub fn clamp(self, x: f32) -> f32 {
x.clamp(self.min, self.max)
}
/// Flip `min` and `max` if needed, so that `min <= max` after.
#[inline]
pub fn as_positive(self) -> Self {
Rangef {
min: self.min.min(self.max),
max: self.min.max(self.max),
}
}
/// Shrink by this much on each side, keeping the center
#[inline]
#[must_use]
pub fn shrink(self, amnt: f32) -> Self {
Self {
min: self.min + amnt,
max: self.max - amnt,
}
}
/// Expand by this much on each side, keeping the center
#[inline]
#[must_use]
pub fn expand(self, amnt: f32) -> Self {
Self {
min: self.min - amnt,
max: self.max + amnt,
}
}
}
impl From<Rangef> for RangeInclusive<f32> {
@@ -108,3 +154,17 @@ impl From<RangeToInclusive<f32>> for Rangef {
Self::new(f32::NEG_INFINITY, range.end)
}
}
impl PartialEq<RangeInclusive<f32>> for Rangef {
#[inline]
fn eq(&self, other: &RangeInclusive<f32>) -> bool {
self.min == *other.start() && self.max == *other.end()
}
}
impl PartialEq<Rangef> for RangeInclusive<f32> {
#[inline]
fn eq(&self, other: &Rangef) -> bool {
*self.start() == other.min && *self.end() == other.max
}
}

View File

@@ -1,5 +1,4 @@
use std::f32::INFINITY;
use std::ops::RangeInclusive;
use crate::*;
@@ -82,15 +81,12 @@ impl Rect {
}
#[inline(always)]
pub fn from_x_y_ranges(
x_range: impl Into<RangeInclusive<f32>>,
y_range: impl Into<RangeInclusive<f32>>,
) -> Self {
pub fn from_x_y_ranges(x_range: impl Into<Rangef>, y_range: impl Into<Rangef>) -> Self {
let x_range = x_range.into();
let y_range = y_range.into();
Rect {
min: pos2(*x_range.start(), *y_range.start()),
max: pos2(*x_range.end(), *y_range.end()),
min: pos2(x_range.min, y_range.min),
max: pos2(x_range.max, y_range.max),
}
}
@@ -280,6 +276,7 @@ impl Rect {
}
}
/// `rect.size() == Vec2 { x: rect.width(), y: rect.height() }`
#[inline(always)]
pub fn size(&self) -> Vec2 {
self.max - self.min
@@ -389,18 +386,18 @@ impl Rect {
}
#[inline(always)]
pub fn x_range(&self) -> RangeInclusive<f32> {
self.min.x..=self.max.x
pub fn x_range(&self) -> Rangef {
Rangef::new(self.min.x, self.max.x)
}
#[inline(always)]
pub fn y_range(&self) -> RangeInclusive<f32> {
self.min.y..=self.max.y
pub fn y_range(&self) -> Rangef {
Rangef::new(self.min.y, self.max.y)
}
#[inline(always)]
pub fn bottom_up_range(&self) -> RangeInclusive<f32> {
self.max.y..=self.min.y
pub fn bottom_up_range(&self) -> Rangef {
Rangef::new(self.max.y, self.min.y)
}
/// `width < 0 || height < 0`

View File

@@ -184,10 +184,7 @@ mod test {
let expected = vec2(0.0, 3.0);
assert!(
(rotated - expected).length() < 1e-5,
"Expected {:?} to equal {:?}. rot: {:?}",
rotated,
expected,
rot,
"Expected {rotated:?} to equal {expected:?}. rot: {rot:?}",
);
let undone = rot.inverse() * rot;

View File

@@ -292,7 +292,7 @@ impl std::ops::Index<usize> for Vec2 {
match index {
0 => &self.x,
1 => &self.y,
_ => panic!("Vec2 index out of bounds: {}", index),
_ => panic!("Vec2 index out of bounds: {index}"),
}
}
}
@@ -303,7 +303,7 @@ impl std::ops::IndexMut<usize> for Vec2 {
match index {
0 => &mut self.x,
1 => &mut self.y,
_ => panic!("Vec2 index out of bounds: {}", index),
_ => panic!("Vec2 index out of bounds: {index}"),
}
}
}

View File

@@ -1,8 +1,9 @@
# epaint changelog
All notable changes to the epaint crate will be documented in this file.
This file is updated upon each release.
Changes since the last release can be found by running the `scripts/generate_changelog.py` script.
## Unreleased
## 0.22.0 - 2023-05-23

View File

@@ -4,7 +4,7 @@ version = "0.22.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "Minimal 2D graphics library for GUI work"
edition = "2021"
rust-version = "1.65"
rust-version = "1.67"
homepage = "https://github.com/emilk/egui/tree/master/crates/epaint"
license = "(MIT OR Apache-2.0) AND OFL-1.1 AND LicenseRef-UFL-1.0" # OFL and UFL used by default_fonts. See https://github.com/emilk/egui/issues/2321
readme = "README.md"
@@ -79,6 +79,7 @@ ahash = { version = "0.8.1", default-features = false, features = [
"std",
] }
nohash-hasher = "0.2"
parking_lot = "0.12" # Using parking_lot over std::sync::Mutex gives 50% speedups in some real-world scenarios.
#! ### Optional dependencies
bytemuck = { version = "1.7.2", optional = true, features = ["derive"] }
@@ -94,11 +95,6 @@ serde = { version = "1", optional = true, features = ["derive", "rc"] }
# native:
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
backtrace = { version = "0.3", optional = true }
parking_lot = "0.12" # Using parking_lot over std::sync::Mutex gives 50% speedups in some real-world scenarios.
# web:
[target.'cfg(target_arch = "wasm32")'.dependencies]
atomic_refcell = "0.1" # Used instead of parking_lot on on wasm. See https://github.com/emilk/egui/issues/1401
[dev-dependencies]

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