mirror of
https://github.com/emilk/egui.git
synced 2026-08-30 13:20:05 -04:00
Merge branch 'master' into feat-window-actions
This commit is contained in:
10
crates/ecolor/CHANGELOG.md
Normal file
10
crates/ecolor/CHANGELOG.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Changelog for ecolor
|
||||
All notable changes to the `ecolor` crate will be noted in this file.
|
||||
|
||||
|
||||
## Unreleased
|
||||
* Add `Color32::gamma_multiply` ([#2437](https://github.com/emilk/egui/pull/2437)).
|
||||
|
||||
|
||||
## 0.20.0 - 2022-12-08
|
||||
* Split out `ecolor` crate from `epaint`
|
||||
50
crates/ecolor/Cargo.toml
Normal file
50
crates/ecolor/Cargo.toml
Normal file
@@ -0,0 +1,50 @@
|
||||
[package]
|
||||
name = "ecolor"
|
||||
version = "0.20.0"
|
||||
authors = [
|
||||
"Emil Ernerfeldt <emil.ernerfeldt@gmail.com>",
|
||||
"Andreas Reich <reichandreas@gmx.de>",
|
||||
]
|
||||
description = "Color structs and color conversion utilities"
|
||||
edition = "2021"
|
||||
rust-version = "1.65"
|
||||
homepage = "https://github.com/emilk/egui"
|
||||
license = "MIT OR Apache-2.0"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/emilk/egui"
|
||||
categories = ["mathematics", "encoding"]
|
||||
keywords = ["gui", "color", "conversion", "gamedev", "images"]
|
||||
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
||||
[lib]
|
||||
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
## Enable additional checks if debug assertions are enabled (debug builds).
|
||||
extra_debug_asserts = []
|
||||
## Always enable additional checks.
|
||||
extra_asserts = []
|
||||
|
||||
|
||||
[dependencies]
|
||||
#! ### Optional dependencies
|
||||
|
||||
## [`bytemuck`](https://docs.rs/bytemuck) enables you to cast `ecolor` types to `&[u8]`.
|
||||
bytemuck = { version = "1.7.2", optional = true, features = ["derive"] }
|
||||
|
||||
## [`cint`](https://docs.rs/cint) enables interopability with other color libraries.
|
||||
cint = { version = "0.3.1", optional = true }
|
||||
|
||||
## Enable the [`hex_color`] macro.
|
||||
color-hex = { version = "0.2.0", optional = true }
|
||||
|
||||
## Enable this when generating docs.
|
||||
document-features = { version = "0.2", optional = true }
|
||||
|
||||
## Allow serialization using [`serde`](https://docs.rs/serde).
|
||||
serde = { version = "1", optional = true, features = ["derive"] }
|
||||
5
crates/ecolor/README.md
Normal file
5
crates/ecolor/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# ecolor - egui color library
|
||||
|
||||
A simple color storage and conversion library.
|
||||
|
||||
Made for [`egui`](https://github.com/emilk/egui/).
|
||||
161
crates/ecolor/src/cint_impl.rs
Normal file
161
crates/ecolor/src/cint_impl.rs
Normal file
@@ -0,0 +1,161 @@
|
||||
use super::*;
|
||||
use cint::{Alpha, ColorInterop, EncodedSrgb, Hsv, LinearSrgb, PremultipliedAlpha};
|
||||
|
||||
// ---- Color32 ----
|
||||
|
||||
impl From<Alpha<EncodedSrgb<u8>>> for Color32 {
|
||||
fn from(srgba: Alpha<EncodedSrgb<u8>>) -> Self {
|
||||
let Alpha {
|
||||
color: EncodedSrgb { r, g, b },
|
||||
alpha: a,
|
||||
} = srgba;
|
||||
|
||||
Color32::from_rgba_unmultiplied(r, g, b, a)
|
||||
}
|
||||
}
|
||||
|
||||
// No From<Color32> for Alpha<_> because Color32 is premultiplied
|
||||
|
||||
impl From<PremultipliedAlpha<EncodedSrgb<u8>>> for Color32 {
|
||||
fn from(srgba: PremultipliedAlpha<EncodedSrgb<u8>>) -> Self {
|
||||
let PremultipliedAlpha {
|
||||
color: EncodedSrgb { r, g, b },
|
||||
alpha: a,
|
||||
} = srgba;
|
||||
|
||||
Color32::from_rgba_premultiplied(r, g, b, a)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color32> for PremultipliedAlpha<EncodedSrgb<u8>> {
|
||||
fn from(col: Color32) -> Self {
|
||||
let (r, g, b, a) = col.to_tuple();
|
||||
|
||||
PremultipliedAlpha {
|
||||
color: EncodedSrgb { r, g, b },
|
||||
alpha: a,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PremultipliedAlpha<EncodedSrgb<f32>>> for Color32 {
|
||||
fn from(srgba: PremultipliedAlpha<EncodedSrgb<f32>>) -> Self {
|
||||
let PremultipliedAlpha {
|
||||
color: EncodedSrgb { r, g, b },
|
||||
alpha: a,
|
||||
} = srgba;
|
||||
|
||||
// This is a bit of an abuse of the function name but it does what we want.
|
||||
let r = linear_u8_from_linear_f32(r);
|
||||
let g = linear_u8_from_linear_f32(g);
|
||||
let b = linear_u8_from_linear_f32(b);
|
||||
let a = linear_u8_from_linear_f32(a);
|
||||
|
||||
Color32::from_rgba_premultiplied(r, g, b, a)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color32> for PremultipliedAlpha<EncodedSrgb<f32>> {
|
||||
fn from(col: Color32) -> Self {
|
||||
let (r, g, b, a) = col.to_tuple();
|
||||
|
||||
// This is a bit of an abuse of the function name but it does what we want.
|
||||
let r = linear_f32_from_linear_u8(r);
|
||||
let g = linear_f32_from_linear_u8(g);
|
||||
let b = linear_f32_from_linear_u8(b);
|
||||
let a = linear_f32_from_linear_u8(a);
|
||||
|
||||
PremultipliedAlpha {
|
||||
color: EncodedSrgb { r, g, b },
|
||||
alpha: a,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ColorInterop for Color32 {
|
||||
type CintTy = PremultipliedAlpha<EncodedSrgb<u8>>;
|
||||
}
|
||||
|
||||
// ---- Rgba ----
|
||||
|
||||
impl From<PremultipliedAlpha<LinearSrgb<f32>>> for Rgba {
|
||||
fn from(srgba: PremultipliedAlpha<LinearSrgb<f32>>) -> Self {
|
||||
let PremultipliedAlpha {
|
||||
color: LinearSrgb { r, g, b },
|
||||
alpha: a,
|
||||
} = srgba;
|
||||
|
||||
Rgba([r, g, b, a])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Rgba> for PremultipliedAlpha<LinearSrgb<f32>> {
|
||||
fn from(col: Rgba) -> Self {
|
||||
let (r, g, b, a) = col.to_tuple();
|
||||
|
||||
PremultipliedAlpha {
|
||||
color: LinearSrgb { r, g, b },
|
||||
alpha: a,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ColorInterop for Rgba {
|
||||
type CintTy = PremultipliedAlpha<LinearSrgb<f32>>;
|
||||
}
|
||||
|
||||
// ---- Hsva ----
|
||||
|
||||
impl From<Alpha<Hsv<f32>>> for Hsva {
|
||||
fn from(srgba: Alpha<Hsv<f32>>) -> Self {
|
||||
let Alpha {
|
||||
color: Hsv { h, s, v },
|
||||
alpha: a,
|
||||
} = srgba;
|
||||
|
||||
Hsva::new(h, s, v, a)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Hsva> for Alpha<Hsv<f32>> {
|
||||
fn from(col: Hsva) -> Self {
|
||||
let Hsva { h, s, v, a } = col;
|
||||
|
||||
Alpha {
|
||||
color: Hsv { h, s, v },
|
||||
alpha: a,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ColorInterop for Hsva {
|
||||
type CintTy = Alpha<Hsv<f32>>;
|
||||
}
|
||||
|
||||
// ---- HsvaGamma ----
|
||||
|
||||
impl ColorInterop for HsvaGamma {
|
||||
type CintTy = Alpha<Hsv<f32>>;
|
||||
}
|
||||
|
||||
impl From<Alpha<Hsv<f32>>> for HsvaGamma {
|
||||
fn from(srgba: Alpha<Hsv<f32>>) -> Self {
|
||||
let Alpha {
|
||||
color: Hsv { h, s, v },
|
||||
alpha: a,
|
||||
} = srgba;
|
||||
|
||||
Hsva::new(h, s, v, a).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HsvaGamma> for Alpha<Hsv<f32>> {
|
||||
fn from(col: HsvaGamma) -> Self {
|
||||
let Hsva { h, s, v, a } = col.into();
|
||||
|
||||
Alpha {
|
||||
color: Hsv { h, s, v },
|
||||
alpha: a,
|
||||
}
|
||||
}
|
||||
}
|
||||
216
crates/ecolor/src/color32.rs
Normal file
216
crates/ecolor/src/color32.rs
Normal file
@@ -0,0 +1,216 @@
|
||||
use crate::{gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_f32_from_linear_u8, Rgba};
|
||||
|
||||
/// This format is used for space-efficient color representation (32 bits).
|
||||
///
|
||||
/// Instead of manipulating this directly it is often better
|
||||
/// to first convert it to either [`Rgba`] or [`crate::Hsva`].
|
||||
///
|
||||
/// Internally this uses 0-255 gamma space `sRGBA` color with premultiplied alpha.
|
||||
/// Alpha channel is in linear space.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))]
|
||||
pub struct Color32(pub(crate) [u8; 4]);
|
||||
|
||||
impl std::ops::Index<usize> for Color32 {
|
||||
type Output = u8;
|
||||
|
||||
#[inline(always)]
|
||||
fn index(&self, index: usize) -> &u8 {
|
||||
&self.0[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::IndexMut<usize> for Color32 {
|
||||
#[inline(always)]
|
||||
fn index_mut(&mut self, index: usize) -> &mut u8 {
|
||||
&mut self.0[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl Color32 {
|
||||
// Mostly follows CSS names:
|
||||
|
||||
pub const TRANSPARENT: Color32 = Color32::from_rgba_premultiplied(0, 0, 0, 0);
|
||||
pub const BLACK: Color32 = Color32::from_rgb(0, 0, 0);
|
||||
pub const DARK_GRAY: Color32 = Color32::from_rgb(96, 96, 96);
|
||||
pub const GRAY: Color32 = Color32::from_rgb(160, 160, 160);
|
||||
pub const LIGHT_GRAY: Color32 = Color32::from_rgb(220, 220, 220);
|
||||
pub const WHITE: Color32 = Color32::from_rgb(255, 255, 255);
|
||||
|
||||
pub const BROWN: Color32 = Color32::from_rgb(165, 42, 42);
|
||||
pub const DARK_RED: Color32 = Color32::from_rgb(0x8B, 0, 0);
|
||||
pub const RED: Color32 = Color32::from_rgb(255, 0, 0);
|
||||
pub const LIGHT_RED: Color32 = Color32::from_rgb(255, 128, 128);
|
||||
|
||||
pub const YELLOW: Color32 = Color32::from_rgb(255, 255, 0);
|
||||
pub const LIGHT_YELLOW: Color32 = Color32::from_rgb(255, 255, 0xE0);
|
||||
pub const KHAKI: Color32 = Color32::from_rgb(240, 230, 140);
|
||||
|
||||
pub const DARK_GREEN: Color32 = Color32::from_rgb(0, 0x64, 0);
|
||||
pub const GREEN: Color32 = Color32::from_rgb(0, 255, 0);
|
||||
pub const LIGHT_GREEN: Color32 = Color32::from_rgb(0x90, 0xEE, 0x90);
|
||||
|
||||
pub const DARK_BLUE: Color32 = Color32::from_rgb(0, 0, 0x8B);
|
||||
pub const BLUE: Color32 = Color32::from_rgb(0, 0, 255);
|
||||
pub const LIGHT_BLUE: Color32 = Color32::from_rgb(0xAD, 0xD8, 0xE6);
|
||||
|
||||
pub const GOLD: Color32 = Color32::from_rgb(255, 215, 0);
|
||||
|
||||
pub const DEBUG_COLOR: Color32 = Color32::from_rgba_premultiplied(0, 200, 0, 128);
|
||||
|
||||
/// An ugly color that is planned to be replaced before making it to the screen.
|
||||
pub const TEMPORARY_COLOR: Color32 = Color32::from_rgb(64, 254, 0);
|
||||
|
||||
#[inline(always)]
|
||||
pub const fn from_rgb(r: u8, g: u8, b: u8) -> Self {
|
||||
Self([r, g, b, 255])
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub const fn from_rgb_additive(r: u8, g: u8, b: u8) -> Self {
|
||||
Self([r, g, b, 0])
|
||||
}
|
||||
|
||||
/// From `sRGBA` with premultiplied alpha.
|
||||
#[inline(always)]
|
||||
pub const fn from_rgba_premultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||
Self([r, g, b, a])
|
||||
}
|
||||
|
||||
/// From `sRGBA` WITHOUT premultiplied alpha.
|
||||
pub fn from_rgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||
if a == 255 {
|
||||
Self::from_rgb(r, g, b) // common-case optimization
|
||||
} else if a == 0 {
|
||||
Self::TRANSPARENT // common-case optimization
|
||||
} else {
|
||||
let r_lin = linear_f32_from_gamma_u8(r);
|
||||
let g_lin = linear_f32_from_gamma_u8(g);
|
||||
let b_lin = linear_f32_from_gamma_u8(b);
|
||||
let a_lin = linear_f32_from_linear_u8(a);
|
||||
|
||||
let r = gamma_u8_from_linear_f32(r_lin * a_lin);
|
||||
let g = gamma_u8_from_linear_f32(g_lin * a_lin);
|
||||
let b = gamma_u8_from_linear_f32(b_lin * a_lin);
|
||||
|
||||
Self::from_rgba_premultiplied(r, g, b, a)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub const fn from_gray(l: u8) -> Self {
|
||||
Self([l, l, l, 255])
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub const fn from_black_alpha(a: u8) -> Self {
|
||||
Self([0, 0, 0, a])
|
||||
}
|
||||
|
||||
pub fn from_white_alpha(a: u8) -> Self {
|
||||
Rgba::from_white_alpha(linear_f32_from_linear_u8(a)).into()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub const fn from_additive_luminance(l: u8) -> Self {
|
||||
Self([l, l, l, 0])
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub const fn is_opaque(&self) -> bool {
|
||||
self.a() == 255
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub const fn r(&self) -> u8 {
|
||||
self.0[0]
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub const fn g(&self) -> u8 {
|
||||
self.0[1]
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub const fn b(&self) -> u8 {
|
||||
self.0[2]
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub const fn a(&self) -> u8 {
|
||||
self.0[3]
|
||||
}
|
||||
|
||||
/// Returns an opaque version of self
|
||||
pub fn to_opaque(self) -> Self {
|
||||
Rgba::from(self).to_opaque().into()
|
||||
}
|
||||
|
||||
/// Returns an additive version of self
|
||||
#[inline(always)]
|
||||
pub const fn additive(self) -> Self {
|
||||
let [r, g, b, _] = self.to_array();
|
||||
Self([r, g, b, 0])
|
||||
}
|
||||
|
||||
/// Premultiplied RGBA
|
||||
#[inline(always)]
|
||||
pub const fn to_array(&self) -> [u8; 4] {
|
||||
[self.r(), self.g(), self.b(), self.a()]
|
||||
}
|
||||
|
||||
/// Premultiplied RGBA
|
||||
#[inline(always)]
|
||||
pub const fn to_tuple(&self) -> (u8, u8, u8, u8) {
|
||||
(self.r(), self.g(), self.b(), self.a())
|
||||
}
|
||||
|
||||
pub fn to_srgba_unmultiplied(&self) -> [u8; 4] {
|
||||
Rgba::from(*self).to_srgba_unmultiplied()
|
||||
}
|
||||
|
||||
/// Multiply with 0.5 to make color half as opaque, perceptually.
|
||||
///
|
||||
/// Fast multiplication in gamma-space.
|
||||
///
|
||||
/// This is perceptually even, and faster that [`Self::linear_multiply`].
|
||||
#[inline]
|
||||
pub fn gamma_multiply(self, factor: f32) -> Color32 {
|
||||
crate::ecolor_assert!(0.0 <= factor && factor <= 1.0);
|
||||
let Self([r, g, b, a]) = self;
|
||||
Self([
|
||||
(r as f32 * factor + 0.5) as u8,
|
||||
(g as f32 * factor + 0.5) as u8,
|
||||
(b as f32 * factor + 0.5) as u8,
|
||||
(a as f32 * factor + 0.5) as u8,
|
||||
])
|
||||
}
|
||||
|
||||
/// Multiply with 0.5 to make color half as opaque in linear space.
|
||||
///
|
||||
/// This is using linear space, which is not perceptually even.
|
||||
/// You may want to use [`Self::gamma_multiply`] instead.
|
||||
pub fn linear_multiply(self, factor: f32) -> Color32 {
|
||||
crate::ecolor_assert!(0.0 <= factor && factor <= 1.0);
|
||||
// As an unfortunate side-effect of using premultiplied alpha
|
||||
// we need a somewhat expensive conversion to linear space and back.
|
||||
Rgba::from(self).multiply(factor).into()
|
||||
}
|
||||
|
||||
/// Converts to floating point values in the range 0-1 without any gamma space conversion.
|
||||
///
|
||||
/// Use this with great care! In almost all cases, you want to convert to [`crate::Rgba`] instead
|
||||
/// in order to obtain linear space color values.
|
||||
#[inline]
|
||||
pub fn to_normalized_gamma_f32(self) -> [f32; 4] {
|
||||
let Self([r, g, b, a]) = self;
|
||||
[
|
||||
r as f32 / 255.0,
|
||||
g as f32 / 255.0,
|
||||
b as f32 / 255.0,
|
||||
a as f32 / 255.0,
|
||||
]
|
||||
}
|
||||
}
|
||||
39
crates/ecolor/src/hex_color_macro.rs
Normal file
39
crates/ecolor/src/hex_color_macro.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
/// Construct a [`crate::Color32`] from a hex RGB or RGBA string.
|
||||
///
|
||||
/// ```
|
||||
/// # use ecolor::{hex_color, Color32};
|
||||
/// assert_eq!(hex_color!("#202122"), Color32::from_rgb(0x20, 0x21, 0x22));
|
||||
/// assert_eq!(hex_color!("#abcdef12"), Color32::from_rgba_unmultiplied(0xab, 0xcd, 0xef, 0x12));
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! hex_color {
|
||||
($s:literal) => {{
|
||||
let array = color_hex::color_from_hex!($s);
|
||||
if array.len() == 3 {
|
||||
$crate::Color32::from_rgb(array[0], array[1], array[2])
|
||||
} else {
|
||||
#[allow(unconditional_panic)]
|
||||
$crate::Color32::from_rgba_unmultiplied(array[0], array[1], array[2], array[3])
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_rgb_hex() {
|
||||
assert_eq!(
|
||||
crate::Color32::from_rgb(0x20, 0x21, 0x22),
|
||||
hex_color!("#202122")
|
||||
);
|
||||
assert_eq!(
|
||||
crate::Color32::from_rgb_additive(0x20, 0x21, 0x22),
|
||||
hex_color!("#202122").additive()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_rgba_hex() {
|
||||
assert_eq!(
|
||||
crate::Color32::from_rgba_unmultiplied(0x20, 0x21, 0x22, 0x50),
|
||||
hex_color!("20212250")
|
||||
);
|
||||
}
|
||||
231
crates/ecolor/src/hsva.rs
Normal file
231
crates/ecolor/src/hsva.rs
Normal file
@@ -0,0 +1,231 @@
|
||||
use crate::{
|
||||
gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_f32_from_linear_u8,
|
||||
linear_u8_from_linear_f32, Color32, Rgba,
|
||||
};
|
||||
|
||||
/// Hue, saturation, value, alpha. All in the range [0, 1].
|
||||
/// No premultiplied alpha.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct Hsva {
|
||||
/// hue 0-1
|
||||
pub h: f32,
|
||||
|
||||
/// saturation 0-1
|
||||
pub s: f32,
|
||||
|
||||
/// value 0-1
|
||||
pub v: f32,
|
||||
|
||||
/// alpha 0-1. A negative value signifies an additive color (and alpha is ignored).
|
||||
pub a: f32,
|
||||
}
|
||||
|
||||
impl Hsva {
|
||||
pub fn new(h: f32, s: f32, v: f32, a: f32) -> Self {
|
||||
Self { h, s, v, a }
|
||||
}
|
||||
|
||||
/// From `sRGBA` with premultiplied alpha
|
||||
pub fn from_srgba_premultiplied(srgba: [u8; 4]) -> Self {
|
||||
Self::from_rgba_premultiplied(
|
||||
linear_f32_from_gamma_u8(srgba[0]),
|
||||
linear_f32_from_gamma_u8(srgba[1]),
|
||||
linear_f32_from_gamma_u8(srgba[2]),
|
||||
linear_f32_from_linear_u8(srgba[3]),
|
||||
)
|
||||
}
|
||||
|
||||
/// From `sRGBA` without premultiplied alpha
|
||||
pub fn from_srgba_unmultiplied(srgba: [u8; 4]) -> Self {
|
||||
Self::from_rgba_unmultiplied(
|
||||
linear_f32_from_gamma_u8(srgba[0]),
|
||||
linear_f32_from_gamma_u8(srgba[1]),
|
||||
linear_f32_from_gamma_u8(srgba[2]),
|
||||
linear_f32_from_linear_u8(srgba[3]),
|
||||
)
|
||||
}
|
||||
|
||||
/// From linear RGBA with premultiplied alpha
|
||||
pub fn from_rgba_premultiplied(r: f32, g: f32, b: f32, a: f32) -> Self {
|
||||
#![allow(clippy::many_single_char_names)]
|
||||
if a == 0.0 {
|
||||
if r == 0.0 && b == 0.0 && a == 0.0 {
|
||||
Hsva::default()
|
||||
} else {
|
||||
Hsva::from_additive_rgb([r, g, b])
|
||||
}
|
||||
} else {
|
||||
let (h, s, v) = hsv_from_rgb([r / a, g / a, b / a]);
|
||||
Hsva { h, s, v, a }
|
||||
}
|
||||
}
|
||||
|
||||
/// From linear RGBA without premultiplied alpha
|
||||
pub fn from_rgba_unmultiplied(r: f32, g: f32, b: f32, a: f32) -> Self {
|
||||
#![allow(clippy::many_single_char_names)]
|
||||
let (h, s, v) = hsv_from_rgb([r, g, b]);
|
||||
Hsva { h, s, v, a }
|
||||
}
|
||||
|
||||
pub fn from_additive_rgb(rgb: [f32; 3]) -> Self {
|
||||
let (h, s, v) = hsv_from_rgb(rgb);
|
||||
Hsva {
|
||||
h,
|
||||
s,
|
||||
v,
|
||||
a: -0.5, // anything negative is treated as additive
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_rgb(rgb: [f32; 3]) -> Self {
|
||||
let (h, s, v) = hsv_from_rgb(rgb);
|
||||
Hsva { h, s, v, a: 1.0 }
|
||||
}
|
||||
|
||||
pub fn from_srgb([r, g, b]: [u8; 3]) -> Self {
|
||||
Self::from_rgb([
|
||||
linear_f32_from_gamma_u8(r),
|
||||
linear_f32_from_gamma_u8(g),
|
||||
linear_f32_from_gamma_u8(b),
|
||||
])
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
pub fn to_opaque(self) -> Self {
|
||||
Self { a: 1.0, ..self }
|
||||
}
|
||||
|
||||
pub fn to_rgb(&self) -> [f32; 3] {
|
||||
rgb_from_hsv((self.h, self.s, self.v))
|
||||
}
|
||||
|
||||
pub fn to_srgb(&self) -> [u8; 3] {
|
||||
let [r, g, b] = self.to_rgb();
|
||||
[
|
||||
gamma_u8_from_linear_f32(r),
|
||||
gamma_u8_from_linear_f32(g),
|
||||
gamma_u8_from_linear_f32(b),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn to_rgba_premultiplied(&self) -> [f32; 4] {
|
||||
let [r, g, b, a] = self.to_rgba_unmultiplied();
|
||||
let additive = a < 0.0;
|
||||
if additive {
|
||||
[r, g, b, 0.0]
|
||||
} else {
|
||||
[a * r, a * g, a * b, a]
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents additive colors using a negative alpha.
|
||||
pub fn to_rgba_unmultiplied(&self) -> [f32; 4] {
|
||||
let Hsva { h, s, v, a } = *self;
|
||||
let [r, g, b] = rgb_from_hsv((h, s, v));
|
||||
[r, g, b, a]
|
||||
}
|
||||
|
||||
pub fn to_srgba_premultiplied(&self) -> [u8; 4] {
|
||||
let [r, g, b, a] = self.to_rgba_premultiplied();
|
||||
[
|
||||
gamma_u8_from_linear_f32(r),
|
||||
gamma_u8_from_linear_f32(g),
|
||||
gamma_u8_from_linear_f32(b),
|
||||
linear_u8_from_linear_f32(a),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn to_srgba_unmultiplied(&self) -> [u8; 4] {
|
||||
let [r, g, b, a] = self.to_rgba_unmultiplied();
|
||||
[
|
||||
gamma_u8_from_linear_f32(r),
|
||||
gamma_u8_from_linear_f32(g),
|
||||
gamma_u8_from_linear_f32(b),
|
||||
linear_u8_from_linear_f32(a.abs()),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Hsva> for Rgba {
|
||||
fn from(hsva: Hsva) -> Rgba {
|
||||
Rgba(hsva.to_rgba_premultiplied())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Rgba> for Hsva {
|
||||
fn from(rgba: Rgba) -> Hsva {
|
||||
Self::from_rgba_premultiplied(rgba.0[0], rgba.0[1], rgba.0[2], rgba.0[3])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Hsva> for Color32 {
|
||||
fn from(hsva: Hsva) -> Color32 {
|
||||
Color32::from(Rgba::from(hsva))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color32> for Hsva {
|
||||
fn from(srgba: Color32) -> Hsva {
|
||||
Hsva::from(Rgba::from(srgba))
|
||||
}
|
||||
}
|
||||
|
||||
/// All ranges in 0-1, rgb is linear.
|
||||
pub fn hsv_from_rgb([r, g, b]: [f32; 3]) -> (f32, f32, f32) {
|
||||
#![allow(clippy::many_single_char_names)]
|
||||
let min = r.min(g.min(b));
|
||||
let max = r.max(g.max(b)); // value
|
||||
|
||||
let range = max - min;
|
||||
|
||||
let h = if max == min {
|
||||
0.0 // hue is undefined
|
||||
} else if max == r {
|
||||
(g - b) / (6.0 * range)
|
||||
} else if max == g {
|
||||
(b - r) / (6.0 * range) + 1.0 / 3.0
|
||||
} else {
|
||||
// max == b
|
||||
(r - g) / (6.0 * range) + 2.0 / 3.0
|
||||
};
|
||||
let h = (h + 1.0).fract(); // wrap
|
||||
let s = if max == 0.0 { 0.0 } else { 1.0 - min / max };
|
||||
(h, s, max)
|
||||
}
|
||||
|
||||
/// All ranges in 0-1, rgb is linear.
|
||||
pub fn rgb_from_hsv((h, s, v): (f32, f32, f32)) -> [f32; 3] {
|
||||
#![allow(clippy::many_single_char_names)]
|
||||
let h = (h.fract() + 1.0).fract(); // wrap
|
||||
let s = s.clamp(0.0, 1.0);
|
||||
|
||||
let f = h * 6.0 - (h * 6.0).floor();
|
||||
let p = v * (1.0 - s);
|
||||
let q = v * (1.0 - f * s);
|
||||
let t = v * (1.0 - (1.0 - f) * s);
|
||||
|
||||
match (h * 6.0).floor() as i32 % 6 {
|
||||
0 => [v, t, p],
|
||||
1 => [q, v, p],
|
||||
2 => [p, v, t],
|
||||
3 => [p, q, v],
|
||||
4 => [t, p, v],
|
||||
5 => [v, p, q],
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // a bit expensive
|
||||
fn test_hsv_roundtrip() {
|
||||
for r in 0..=255 {
|
||||
for g in 0..=255 {
|
||||
for b in 0..=255 {
|
||||
let srgba = Color32::from_rgb(r, g, b);
|
||||
let hsva = Hsva::from(srgba);
|
||||
assert_eq!(srgba, Color32::from(hsva));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
66
crates/ecolor/src/hsva_gamma.rs
Normal file
66
crates/ecolor/src/hsva_gamma.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use crate::{gamma_from_linear, linear_from_gamma, Color32, Hsva, Rgba};
|
||||
|
||||
/// Like Hsva but with the `v` value (brightness) being gamma corrected
|
||||
/// so that it is somewhat perceptually even.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct HsvaGamma {
|
||||
/// hue 0-1
|
||||
pub h: f32,
|
||||
|
||||
/// saturation 0-1
|
||||
pub s: f32,
|
||||
|
||||
/// value 0-1, in gamma-space (~perceptually even)
|
||||
pub v: f32,
|
||||
|
||||
/// alpha 0-1. A negative value signifies an additive color (and alpha is ignored).
|
||||
pub a: f32,
|
||||
}
|
||||
|
||||
impl From<HsvaGamma> for Rgba {
|
||||
fn from(hsvag: HsvaGamma) -> Rgba {
|
||||
Hsva::from(hsvag).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HsvaGamma> for Color32 {
|
||||
fn from(hsvag: HsvaGamma) -> Color32 {
|
||||
Rgba::from(hsvag).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HsvaGamma> for Hsva {
|
||||
fn from(hsvag: HsvaGamma) -> Hsva {
|
||||
let HsvaGamma { h, s, v, a } = hsvag;
|
||||
Hsva {
|
||||
h,
|
||||
s,
|
||||
v: linear_from_gamma(v),
|
||||
a,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Rgba> for HsvaGamma {
|
||||
fn from(rgba: Rgba) -> HsvaGamma {
|
||||
Hsva::from(rgba).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color32> for HsvaGamma {
|
||||
fn from(srgba: Color32) -> HsvaGamma {
|
||||
Hsva::from(srgba).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Hsva> for HsvaGamma {
|
||||
fn from(hsva: Hsva) -> HsvaGamma {
|
||||
let Hsva { h, s, v, a } = hsva;
|
||||
HsvaGamma {
|
||||
h,
|
||||
s,
|
||||
v: gamma_from_linear(v),
|
||||
a,
|
||||
}
|
||||
}
|
||||
}
|
||||
173
crates/ecolor/src/lib.rs
Normal file
173
crates/ecolor/src/lib.rs
Normal file
@@ -0,0 +1,173 @@
|
||||
//! Color conversions and types.
|
||||
//!
|
||||
//! If you want a compact color representation, use [`Color32`].
|
||||
//! If you want to manipulate RGBA colors use [`Rgba`].
|
||||
//! If you want to manipulate colors in a way closer to how humans think about colors, use [`HsvaGamma`].
|
||||
//!
|
||||
//! ## Feature flags
|
||||
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
|
||||
//!
|
||||
|
||||
#![allow(clippy::wrong_self_convention)]
|
||||
|
||||
#[cfg(feature = "cint")]
|
||||
mod cint_impl;
|
||||
#[cfg(feature = "cint")]
|
||||
pub use cint_impl::*;
|
||||
|
||||
mod color32;
|
||||
pub use color32::*;
|
||||
|
||||
mod hsva_gamma;
|
||||
pub use hsva_gamma::*;
|
||||
|
||||
mod hsva;
|
||||
pub use hsva::*;
|
||||
|
||||
#[cfg(feature = "color-hex")]
|
||||
mod hex_color_macro;
|
||||
|
||||
mod rgba;
|
||||
pub use rgba::*;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Color conversion:
|
||||
|
||||
impl From<Color32> for Rgba {
|
||||
fn from(srgba: Color32) -> Rgba {
|
||||
Rgba([
|
||||
linear_f32_from_gamma_u8(srgba.0[0]),
|
||||
linear_f32_from_gamma_u8(srgba.0[1]),
|
||||
linear_f32_from_gamma_u8(srgba.0[2]),
|
||||
linear_f32_from_linear_u8(srgba.0[3]),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Rgba> for Color32 {
|
||||
fn from(rgba: Rgba) -> Color32 {
|
||||
Color32([
|
||||
gamma_u8_from_linear_f32(rgba.0[0]),
|
||||
gamma_u8_from_linear_f32(rgba.0[1]),
|
||||
gamma_u8_from_linear_f32(rgba.0[2]),
|
||||
linear_u8_from_linear_f32(rgba.0[3]),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
/// gamma [0, 255] -> linear [0, 1].
|
||||
pub fn linear_f32_from_gamma_u8(s: u8) -> f32 {
|
||||
if s <= 10 {
|
||||
s as f32 / 3294.6
|
||||
} else {
|
||||
((s as f32 + 14.025) / 269.025).powf(2.4)
|
||||
}
|
||||
}
|
||||
|
||||
/// linear [0, 255] -> linear [0, 1].
|
||||
/// Useful for alpha-channel.
|
||||
#[inline(always)]
|
||||
pub fn linear_f32_from_linear_u8(a: u8) -> f32 {
|
||||
a as f32 / 255.0
|
||||
}
|
||||
|
||||
/// linear [0, 1] -> gamma [0, 255] (clamped).
|
||||
/// Values outside this range will be clamped to the range.
|
||||
pub fn gamma_u8_from_linear_f32(l: f32) -> u8 {
|
||||
if l <= 0.0 {
|
||||
0
|
||||
} else if l <= 0.0031308 {
|
||||
fast_round(3294.6 * l)
|
||||
} else if l <= 1.0 {
|
||||
fast_round(269.025 * l.powf(1.0 / 2.4) - 14.025)
|
||||
} else {
|
||||
255
|
||||
}
|
||||
}
|
||||
|
||||
/// linear [0, 1] -> linear [0, 255] (clamped).
|
||||
/// Useful for alpha-channel.
|
||||
#[inline(always)]
|
||||
pub fn linear_u8_from_linear_f32(a: f32) -> u8 {
|
||||
fast_round(a * 255.0)
|
||||
}
|
||||
|
||||
fn fast_round(r: f32) -> u8 {
|
||||
(r + 0.5).floor() as _ // rust does a saturating cast since 1.45
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_srgba_conversion() {
|
||||
for b in 0..=255 {
|
||||
let l = linear_f32_from_gamma_u8(b);
|
||||
assert!(0.0 <= l && l <= 1.0);
|
||||
assert_eq!(gamma_u8_from_linear_f32(l), b);
|
||||
}
|
||||
}
|
||||
|
||||
/// gamma [0, 1] -> linear [0, 1] (not clamped).
|
||||
/// Works for numbers outside this range (e.g. negative numbers).
|
||||
pub fn linear_from_gamma(gamma: f32) -> f32 {
|
||||
if gamma < 0.0 {
|
||||
-linear_from_gamma(-gamma)
|
||||
} else if gamma <= 0.04045 {
|
||||
gamma / 12.92
|
||||
} else {
|
||||
((gamma + 0.055) / 1.055).powf(2.4)
|
||||
}
|
||||
}
|
||||
|
||||
/// linear [0, 1] -> gamma [0, 1] (not clamped).
|
||||
/// Works for numbers outside this range (e.g. negative numbers).
|
||||
pub fn gamma_from_linear(linear: f32) -> f32 {
|
||||
if linear < 0.0 {
|
||||
-gamma_from_linear(-linear)
|
||||
} else if linear <= 0.0031308 {
|
||||
12.92 * linear
|
||||
} else {
|
||||
1.055 * linear.powf(1.0 / 2.4) - 0.055
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// An assert that is only active when `epaint` is compiled with the `extra_asserts` feature
|
||||
/// or with the `extra_debug_asserts` feature in debug builds.
|
||||
#[macro_export]
|
||||
macro_rules! ecolor_assert {
|
||||
($($arg: tt)*) => {
|
||||
if cfg!(any(
|
||||
feature = "extra_asserts",
|
||||
all(feature = "extra_debug_asserts", debug_assertions),
|
||||
)) {
|
||||
assert!($($arg)*);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Cheap and ugly.
|
||||
/// Made for graying out disabled `Ui`s.
|
||||
pub fn tint_color_towards(color: Color32, target: Color32) -> Color32 {
|
||||
let [mut r, mut g, mut b, mut a] = color.to_array();
|
||||
|
||||
if a == 0 {
|
||||
r /= 2;
|
||||
g /= 2;
|
||||
b /= 2;
|
||||
} else if a < 170 {
|
||||
// Cheapish and looks ok.
|
||||
// Works for e.g. grid stripes.
|
||||
let div = (2 * 255 / a as i32) as u8;
|
||||
r = r / 2 + target.r() / div;
|
||||
g = g / 2 + target.g() / div;
|
||||
b = b / 2 + target.b() / div;
|
||||
a /= 2;
|
||||
} else {
|
||||
r = r / 2 + target.r() / 2;
|
||||
g = g / 2 + target.g() / 2;
|
||||
b = b / 2 + target.b() / 2;
|
||||
}
|
||||
Color32::from_rgba_premultiplied(r, g, b, a)
|
||||
}
|
||||
266
crates/ecolor/src/rgba.rs
Normal file
266
crates/ecolor/src/rgba.rs
Normal file
@@ -0,0 +1,266 @@
|
||||
use crate::{
|
||||
gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_f32_from_linear_u8,
|
||||
linear_u8_from_linear_f32,
|
||||
};
|
||||
|
||||
/// 0-1 linear space `RGBA` color with premultiplied alpha.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))]
|
||||
pub struct Rgba(pub(crate) [f32; 4]);
|
||||
|
||||
impl std::ops::Index<usize> for Rgba {
|
||||
type Output = f32;
|
||||
|
||||
#[inline(always)]
|
||||
fn index(&self, index: usize) -> &f32 {
|
||||
&self.0[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::IndexMut<usize> for Rgba {
|
||||
#[inline(always)]
|
||||
fn index_mut(&mut self, index: usize) -> &mut f32 {
|
||||
&mut self.0[index]
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn f32_hash<H: std::hash::Hasher>(state: &mut H, f: f32) {
|
||||
if f == 0.0 {
|
||||
state.write_u8(0);
|
||||
} else if f.is_nan() {
|
||||
state.write_u8(1);
|
||||
} else {
|
||||
use std::hash::Hash;
|
||||
f.to_bits().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::derive_hash_xor_eq)]
|
||||
impl std::hash::Hash for Rgba {
|
||||
#[inline]
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
crate::f32_hash(state, self.0[0]);
|
||||
crate::f32_hash(state, self.0[1]);
|
||||
crate::f32_hash(state, self.0[2]);
|
||||
crate::f32_hash(state, self.0[3]);
|
||||
}
|
||||
}
|
||||
|
||||
impl Rgba {
|
||||
pub const TRANSPARENT: Rgba = Rgba::from_rgba_premultiplied(0.0, 0.0, 0.0, 0.0);
|
||||
pub const BLACK: Rgba = Rgba::from_rgb(0.0, 0.0, 0.0);
|
||||
pub const WHITE: Rgba = Rgba::from_rgb(1.0, 1.0, 1.0);
|
||||
pub const RED: Rgba = Rgba::from_rgb(1.0, 0.0, 0.0);
|
||||
pub const GREEN: Rgba = Rgba::from_rgb(0.0, 1.0, 0.0);
|
||||
pub const BLUE: Rgba = Rgba::from_rgb(0.0, 0.0, 1.0);
|
||||
|
||||
#[inline(always)]
|
||||
pub const fn from_rgba_premultiplied(r: f32, g: f32, b: f32, a: f32) -> Self {
|
||||
Self([r, g, b, a])
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn from_rgba_unmultiplied(r: f32, g: f32, b: f32, a: f32) -> Self {
|
||||
Self([r * a, g * a, b * a, a])
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn from_srgba_premultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||
let r = linear_f32_from_gamma_u8(r);
|
||||
let g = linear_f32_from_gamma_u8(g);
|
||||
let b = linear_f32_from_gamma_u8(b);
|
||||
let a = linear_f32_from_linear_u8(a);
|
||||
Self::from_rgba_premultiplied(r, g, b, a)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn from_srgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||
let r = linear_f32_from_gamma_u8(r);
|
||||
let g = linear_f32_from_gamma_u8(g);
|
||||
let b = linear_f32_from_gamma_u8(b);
|
||||
let a = linear_f32_from_linear_u8(a);
|
||||
Self::from_rgba_premultiplied(r * a, g * a, b * a, a)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub const fn from_rgb(r: f32, g: f32, b: f32) -> Self {
|
||||
Self([r, g, b, 1.0])
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub const fn from_gray(l: f32) -> Self {
|
||||
Self([l, l, l, 1.0])
|
||||
}
|
||||
|
||||
pub fn from_luminance_alpha(l: f32, a: f32) -> Self {
|
||||
crate::ecolor_assert!(0.0 <= l && l <= 1.0);
|
||||
crate::ecolor_assert!(0.0 <= a && a <= 1.0);
|
||||
Self([l * a, l * a, l * a, a])
|
||||
}
|
||||
|
||||
/// Transparent black
|
||||
#[inline(always)]
|
||||
pub fn from_black_alpha(a: f32) -> Self {
|
||||
crate::ecolor_assert!(0.0 <= a && a <= 1.0);
|
||||
Self([0.0, 0.0, 0.0, a])
|
||||
}
|
||||
|
||||
/// Transparent white
|
||||
#[inline(always)]
|
||||
pub fn from_white_alpha(a: f32) -> Self {
|
||||
crate::ecolor_assert!(0.0 <= a && a <= 1.0, "a: {}", a);
|
||||
Self([a, a, a, a])
|
||||
}
|
||||
|
||||
/// Return an additive version of this color (alpha = 0)
|
||||
#[inline(always)]
|
||||
pub fn additive(self) -> Self {
|
||||
let [r, g, b, _] = self.0;
|
||||
Self([r, g, b, 0.0])
|
||||
}
|
||||
|
||||
/// Multiply with e.g. 0.5 to make us half transparent
|
||||
#[inline(always)]
|
||||
pub fn multiply(self, alpha: f32) -> Self {
|
||||
Self([
|
||||
alpha * self[0],
|
||||
alpha * self[1],
|
||||
alpha * self[2],
|
||||
alpha * self[3],
|
||||
])
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn r(&self) -> f32 {
|
||||
self.0[0]
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn g(&self) -> f32 {
|
||||
self.0[1]
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn b(&self) -> f32 {
|
||||
self.0[2]
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn a(&self) -> f32 {
|
||||
self.0[3]
|
||||
}
|
||||
|
||||
/// How perceptually intense (bright) is the color?
|
||||
#[inline]
|
||||
pub fn intensity(&self) -> f32 {
|
||||
0.3 * self.r() + 0.59 * self.g() + 0.11 * self.b()
|
||||
}
|
||||
|
||||
/// Returns an opaque version of self
|
||||
pub fn to_opaque(&self) -> Self {
|
||||
if self.a() == 0.0 {
|
||||
// Additive or fully transparent black.
|
||||
Self::from_rgb(self.r(), self.g(), self.b())
|
||||
} else {
|
||||
// un-multiply alpha:
|
||||
Self::from_rgb(
|
||||
self.r() / self.a(),
|
||||
self.g() / self.a(),
|
||||
self.b() / self.a(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Premultiplied RGBA
|
||||
#[inline(always)]
|
||||
pub fn to_array(&self) -> [f32; 4] {
|
||||
[self.r(), self.g(), self.b(), self.a()]
|
||||
}
|
||||
|
||||
/// Premultiplied RGBA
|
||||
#[inline(always)]
|
||||
pub fn to_tuple(&self) -> (f32, f32, f32, f32) {
|
||||
(self.r(), self.g(), self.b(), self.a())
|
||||
}
|
||||
|
||||
/// unmultiply the alpha
|
||||
pub fn to_rgba_unmultiplied(&self) -> [f32; 4] {
|
||||
let a = self.a();
|
||||
if a == 0.0 {
|
||||
// Additive, let's assume we are black
|
||||
self.0
|
||||
} else {
|
||||
[self.r() / a, self.g() / a, self.b() / a, a]
|
||||
}
|
||||
}
|
||||
|
||||
/// unmultiply the alpha
|
||||
pub fn to_srgba_unmultiplied(&self) -> [u8; 4] {
|
||||
let [r, g, b, a] = self.to_rgba_unmultiplied();
|
||||
[
|
||||
gamma_u8_from_linear_f32(r),
|
||||
gamma_u8_from_linear_f32(g),
|
||||
gamma_u8_from_linear_f32(b),
|
||||
linear_u8_from_linear_f32(a.abs()),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Add for Rgba {
|
||||
type Output = Rgba;
|
||||
|
||||
#[inline(always)]
|
||||
fn add(self, rhs: Rgba) -> Rgba {
|
||||
Rgba([
|
||||
self[0] + rhs[0],
|
||||
self[1] + rhs[1],
|
||||
self[2] + rhs[2],
|
||||
self[3] + rhs[3],
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul<Rgba> for Rgba {
|
||||
type Output = Rgba;
|
||||
|
||||
#[inline(always)]
|
||||
fn mul(self, other: Rgba) -> Rgba {
|
||||
Rgba([
|
||||
self[0] * other[0],
|
||||
self[1] * other[1],
|
||||
self[2] * other[2],
|
||||
self[3] * other[3],
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul<f32> for Rgba {
|
||||
type Output = Rgba;
|
||||
|
||||
#[inline(always)]
|
||||
fn mul(self, factor: f32) -> Rgba {
|
||||
Rgba([
|
||||
self[0] * factor,
|
||||
self[1] * factor,
|
||||
self[2] * factor,
|
||||
self[3] * factor,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul<Rgba> for f32 {
|
||||
type Output = Rgba;
|
||||
|
||||
#[inline(always)]
|
||||
fn mul(self, rgba: Rgba) -> Rgba {
|
||||
Rgba([
|
||||
self * rgba[0],
|
||||
self * rgba[1],
|
||||
self * rgba[2],
|
||||
self * rgba[3],
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -5,18 +5,45 @@ NOTE: [`egui-winit`](../egui-winit/CHANGELOG.md), [`egui_glium`](../egui_glium/C
|
||||
|
||||
|
||||
## Unreleased
|
||||
* Added `NativeOptions::fullsize_content` option on Mac to build titlebar-less windows with floating window controls ([#2049](https://github.com/emilk/egui/pull/2049)).
|
||||
* ⚠️ BREAKING: `App::clear_color` now expects you to return a raw float array ([#2666](https://github.com/emilk/egui/pull/2666)).
|
||||
* The `screen_reader` feature has now been renamed `web_screen_reader` and only work on web. On other platforms, use the `accesskit` feature flag instead ([#2669](https://github.com/emilk/egui/pull/2669)).
|
||||
|
||||
#### Desktop/Native:
|
||||
* `eframe::run_native` now returns a `Result` ([#2433](https://github.com/emilk/egui/pull/2433)).
|
||||
* Update to `winit` 0.28, adding support for mac trackpad zoom ([#2654](https://github.com/emilk/egui/pull/2654)).
|
||||
|
||||
#### Web:
|
||||
* Prevent ctrl-P/cmd-P from opening the print dialog ([#2598](https://github.com/emilk/egui/pull/2598)).
|
||||
|
||||
|
||||
## 0.20.1 - 2022-12-11
|
||||
* Fix docs.rs build ([#2420](https://github.com/emilk/egui/pull/2420)).
|
||||
|
||||
|
||||
## 0.20.0 - 2022-12-08 - AccessKit integration and `wgpu` web support
|
||||
* MSRV (Minimum Supported Rust Version) is now `1.65.0` ([#2314](https://github.com/emilk/egui/pull/2314)).
|
||||
* Allow empty textures with the glow renderer.
|
||||
|
||||
#### Desktop/Native:
|
||||
* Don't repaint when just moving window ([#1980](https://github.com/emilk/egui/pull/1980)).
|
||||
* Added `NativeOptions::event_loop_builder` hook for apps to change platform specific event loop options ([#1952](https://github.com/emilk/egui/pull/1952)).
|
||||
* Enabled deferred render state initialization to support Android ([#1952](https://github.com/emilk/egui/pull/1952)).
|
||||
* Allow empty textures with the glow renderer.
|
||||
* Added `shader_version` to `NativeOptions` for cross compiling support on different target OpenGL | ES versions (on native `glow` renderer only) ([#1993](https://github.com/emilk/egui/pull/1993)).
|
||||
* Fix: app state is now saved when user presses Cmd-Q on Mac ([#2013](https://github.com/emilk/egui/pull/2013)).
|
||||
* Added `center` to `NativeOptions` and `monitor_size` to `WindowInfo` on desktop ([#2035](https://github.com/emilk/egui/pull/2035)).
|
||||
* Web: you can access your application from JS using `AppRunner::app_mut`. See `crates/egui_demo_app/src/lib.rs`.
|
||||
* Web: You can now use WebGL on top of `wgpu` by enabling the `wgpu` feature (and disabling `glow` via disabling default features) ([#2107](https://github.com/emilk/egui/pull/2107)).
|
||||
* Web: Add `WebInfo::user_agent` ([#2202](https://github.com/emilk/egui/pull/2202)).
|
||||
* Improve IME support ([#2046](https://github.com/emilk/egui/pull/2046)).
|
||||
* Added mouse-passthrough option ([#2080](https://github.com/emilk/egui/pull/2080)).
|
||||
* Added `NativeOptions::fullsize_content` option on Mac to build titlebar-less windows with floating window controls ([#2049](https://github.com/emilk/egui/pull/2049)).
|
||||
* Wgpu device/adapter/surface creation has now various configuration options exposed via `NativeOptions/WebOptions::wgpu_options` ([#2207](https://github.com/emilk/egui/pull/2207)).
|
||||
* Fix: Make sure that `native_pixels_per_point` is updated ([#2256](https://github.com/emilk/egui/pull/2256)).
|
||||
* Added optional, but enabled by default, integration with [AccessKit](https://accesskit.dev/) for implementing platform accessibility APIs ([#2294](https://github.com/emilk/egui/pull/2294)).
|
||||
* Fix: Less flickering on resize on Windows ([#2280](https://github.com/emilk/egui/pull/2280)).
|
||||
|
||||
#### Web:
|
||||
* ⚠️ BREAKING: `start_web` is a now `async` ([#2107](https://github.com/emilk/egui/pull/2107)).
|
||||
* Web: You can now use WebGL on top of `wgpu` by enabling the `wgpu` feature (and disabling `glow` via disabling default features) ([#2107](https://github.com/emilk/egui/pull/2107)).
|
||||
* Web: Add `WebInfo::user_agent` ([#2202](https://github.com/emilk/egui/pull/2202)).
|
||||
* Web: you can access your application from JS using `AppRunner::app_mut`. See `crates/egui_demo_app/src/lib.rs` ([#1886](https://github.com/emilk/egui/pull/1886)).
|
||||
|
||||
|
||||
## 0.19.0 - 2022-08-20
|
||||
@@ -108,7 +135,7 @@ NOTE: [`egui-winit`](../egui-winit/CHANGELOG.md), [`egui_glium`](../egui_glium/C
|
||||
|
||||
|
||||
## 0.15.0 - 2021-10-24
|
||||
* `Frame` now provides `set_window_title` to set window title dynamically
|
||||
* `Frame` now provides `set_window_title` to set window title dynamically ([#828](https://github.com/emilk/egui/pull/828)).
|
||||
* `Frame` now provides `set_decorations` to set whether to show window decorations.
|
||||
* Remove "http" feature (use https://github.com/emilk/ehttp instead!).
|
||||
* Added `App::persist_native_window` and `App::persist_egui_memory` to control what gets persisted.
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
[package]
|
||||
name = "eframe"
|
||||
version = "0.19.0"
|
||||
version = "0.20.1"
|
||||
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.62"
|
||||
rust-version = "1.65"
|
||||
homepage = "https://github.com/emilk/egui/tree/master/crates/eframe"
|
||||
license = "MIT OR Apache-2.0"
|
||||
readme = "README.md"
|
||||
@@ -15,12 +15,16 @@ include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"]
|
||||
|
||||
[lib]
|
||||
|
||||
|
||||
[features]
|
||||
default = ["default_fonts", "glow"]
|
||||
default = ["accesskit", "default_fonts", "glow"]
|
||||
|
||||
## Enable platform accessibility API implementations through [AccessKit](https://accesskit.dev/).
|
||||
accesskit = ["egui/accesskit", "egui-winit/accesskit"]
|
||||
|
||||
## Detect dark mode system preference using [`dark-light`](https://docs.rs/dark-light).
|
||||
##
|
||||
@@ -32,7 +36,7 @@ dark-light = ["dep:dark-light"]
|
||||
default_fonts = ["egui/default_fonts"]
|
||||
|
||||
## Use [`glow`](https://github.com/grovesNL/glow) for painting, via [`egui_glow`](https://github.com/emilk/egui/tree/master/crates/egui_glow).
|
||||
glow = ["dep:glow", "egui_glow"]
|
||||
glow = ["dep:glow", "dep:egui_glow", "dep:glutin"]
|
||||
|
||||
## Enable saving app state to disk.
|
||||
persistence = [
|
||||
@@ -49,26 +53,33 @@ persistence = [
|
||||
## `eframe` will call `puffin::GlobalProfiler::lock().new_frame()` for you
|
||||
puffin = ["dep:puffin", "egui_glow?/puffin", "egui-wgpu?/puffin"]
|
||||
|
||||
## Enable screen reader support (requires `ctx.options().screen_reader = true;`)
|
||||
screen_reader = ["egui-winit/screen_reader", "tts"]
|
||||
## Enable screen reader support (requires `ctx.options_mut(|o| o.screen_reader = true);`) on web.
|
||||
##
|
||||
## For other platforms, use the "accesskit" feature instead.
|
||||
web_screen_reader = ["tts"]
|
||||
|
||||
## If set, eframe will look for the env-var `EFRAME_SCREENSHOT_TO` and write a screenshot to that location, and then quit.
|
||||
## This is used to generate images for the examples.
|
||||
__screenshot = ["dep:image"]
|
||||
|
||||
## Use [`wgpu`](https://docs.rs/wgpu) for painting (via [`egui-wgpu`](https://github.com/emilk/egui/tree/master/crates/egui-wgpu)).
|
||||
## This overrides the `glow` feature.
|
||||
wgpu = ["dep:wgpu", "dep:egui-wgpu"]
|
||||
wgpu = ["dep:wgpu", "dep:egui-wgpu", "dep:pollster"]
|
||||
|
||||
|
||||
[dependencies]
|
||||
egui = { version = "0.19.0", path = "../egui", default-features = false, features = [
|
||||
egui = { version = "0.20.0", path = "../egui", default-features = false, features = [
|
||||
"bytemuck",
|
||||
"tracing",
|
||||
] }
|
||||
thiserror = "1.0.37"
|
||||
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
||||
|
||||
#! ### Optional dependencies
|
||||
## Enable this when generating docs.
|
||||
document-features = { version = "0.2", optional = true }
|
||||
|
||||
egui_glow = { version = "0.19.0", path = "../egui_glow", optional = true, default-features = false }
|
||||
egui_glow = { version = "0.20.0", path = "../egui_glow", optional = true, default-features = false }
|
||||
glow = { version = "0.11", optional = true }
|
||||
ron = { version = "0.8", optional = true, features = ["integer128"] }
|
||||
serde = { version = "1", optional = true, features = ["derive"] }
|
||||
@@ -76,21 +87,36 @@ serde = { version = "1", optional = true, features = ["derive"] }
|
||||
# -------------------------------------------
|
||||
# native:
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
egui-winit = { version = "0.19.0", path = "../egui-winit", default-features = false, features = [
|
||||
egui-winit = { version = "0.20.0", path = "../egui-winit", default-features = false, features = [
|
||||
"clipboard",
|
||||
"links",
|
||||
] }
|
||||
glutin = { version = "0.29.0" }
|
||||
winit = "0.27.2"
|
||||
raw-window-handle = { version = "0.5.0" }
|
||||
winit = "0.28"
|
||||
|
||||
# optional native:
|
||||
dark-light = { version = "0.2.1", optional = true }
|
||||
dark-light = { version = "1.0", optional = true }
|
||||
directories-next = { version = "2", optional = true }
|
||||
egui-wgpu = { version = "0.19.0", path = "../egui-wgpu", optional = true, features = [
|
||||
egui-wgpu = { version = "0.20.0", path = "../egui-wgpu", optional = true, features = [
|
||||
"winit",
|
||||
] } # if wgpu is used, use it with winit
|
||||
pollster = { version = "0.2", optional = true } # needed for wgpu
|
||||
|
||||
# we can expose these to user so that they can select which backends they want to enable to avoid compiling useless deps.
|
||||
# this can be done at the same time we expose x11/wayland features of winit crate.
|
||||
glutin = { version = "0.30.0", optional = true, es = [
|
||||
"egl",
|
||||
"glx",
|
||||
"x11",
|
||||
"wayland",
|
||||
"wgl",
|
||||
] }
|
||||
|
||||
image = { version = "0.24", optional = true, default-features = false, features = [
|
||||
"png",
|
||||
] }
|
||||
puffin = { version = "0.14", optional = true }
|
||||
wgpu = { version = "0.14", optional = true }
|
||||
wgpu = { version = "0.15.0", optional = true }
|
||||
|
||||
# -------------------------------------------
|
||||
# web:
|
||||
@@ -98,7 +124,7 @@ wgpu = { version = "0.14", optional = true }
|
||||
bytemuck = "1.7"
|
||||
js-sys = "0.3"
|
||||
percent-encoding = "2.1"
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen = "=0.2.83"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
web-sys = { version = "0.3.58", features = [
|
||||
"BinaryType",
|
||||
@@ -144,6 +170,6 @@ web-sys = { version = "0.3.58", features = [
|
||||
] }
|
||||
|
||||
# optional web:
|
||||
egui-wgpu = { version = "0.19.0", path = "../egui-wgpu", optional = true } # if wgpu is used, use it without (!) winit
|
||||
tts = { version = "0.24", optional = true }
|
||||
wgpu = { version = "0.14", optional = true, features = ["webgl"] }
|
||||
egui-wgpu = { version = "0.20.0", path = "../egui-wgpu", optional = true } # if wgpu is used, use it without (!) winit
|
||||
tts = { version = "0.25", optional = true, default-features = false }
|
||||
wgpu = { version = "0.15.0", optional = true, features = ["webgl"] }
|
||||
|
||||
@@ -45,7 +45,6 @@ You can also use `egui_glow` and [`winit`](https://github.com/rust-windowing/win
|
||||
* Mobile text editing is not as good as for a normal web app.
|
||||
* Accessibility: There is an experimental screen reader for `eframe`, but it has to be enabled explicitly. There is no JS function to ask "Does the user want a screen reader?" (and there should probably not be such a function, due to user tracking/integrity concerns).
|
||||
* No integration with browser settings for colors and fonts.
|
||||
* On Linux and Mac, Firefox will copy the WebGL render target from GPU, to CPU and then back again (https://bugzilla.mozilla.org/show_bug.cgi?id=1010527#c0), slowing down egui.
|
||||
|
||||
In many ways, `eframe` is trying to make the browser do something it wasn't designed to do (though there are many things browser vendors could do to improve how well libraries like egui work).
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
use std::any::Any;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use crate::native::run::RequestRepaintEvent;
|
||||
pub use crate::native::run::UserEvent;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use winit::event_loop::EventLoopBuilder;
|
||||
@@ -20,7 +20,7 @@ pub use winit::event_loop::EventLoopBuilder;
|
||||
/// You can configure any platform specific details required on top of the default configuration
|
||||
/// done by `EFrame`.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub type EventLoopBuilderHook = Box<dyn FnOnce(&mut EventLoopBuilder<RequestRepaintEvent>)>;
|
||||
pub type EventLoopBuilderHook = Box<dyn FnOnce(&mut EventLoopBuilder<UserEvent>)>;
|
||||
|
||||
/// This is how your app is created.
|
||||
///
|
||||
@@ -74,7 +74,7 @@ pub trait App {
|
||||
///
|
||||
/// Can be used from web to interact or other external context.
|
||||
///
|
||||
/// You need to implement this if you want to be able to access the application from JS using [`AppRunner::app_mut`].
|
||||
/// You need to implement this if you want to be able to access the application from JS using [`crate::web::backend::AppRunner`].
|
||||
///
|
||||
/// This is needed because downcasting `Box<dyn App>` -> `Box<dyn Any>` to get &`ConcreteApp` is not simple in current rust.
|
||||
///
|
||||
@@ -145,20 +145,25 @@ pub trait App {
|
||||
/// The size limit of the web app canvas.
|
||||
///
|
||||
/// By default the max size is [`egui::Vec2::INFINITY`], i.e. unlimited.
|
||||
///
|
||||
/// A large canvas can lead to bad frame rates on some older browsers on some platforms
|
||||
/// (see <https://bugzilla.mozilla.org/show_bug.cgi?id=1010527#c0>).
|
||||
fn max_size_points(&self) -> egui::Vec2 {
|
||||
egui::Vec2::INFINITY
|
||||
}
|
||||
|
||||
/// Background color for the app, e.g. what is sent to `gl.clearColor`.
|
||||
/// Background color values for the app, e.g. what is sent to `gl.clearColor`.
|
||||
///
|
||||
/// This is the background of your windows if you don't set a central panel.
|
||||
fn clear_color(&self, _visuals: &egui::Visuals) -> egui::Rgba {
|
||||
///
|
||||
/// ATTENTION:
|
||||
/// Since these float values go to the render as-is, any color space conversion as done
|
||||
/// e.g. by converting from [`egui::Color32`] to [`egui::Rgba`] may cause incorrect results.
|
||||
/// egui recommends that rendering backends use a normal "gamma-space" (non-sRGB-aware) blending,
|
||||
/// which means the values you return here should also be in `sRGB` gamma-space in the 0-1 range.
|
||||
/// You can use [`egui::Color32::to_normalized_gamma_f32`] for this.
|
||||
fn clear_color(&self, _visuals: &egui::Visuals) -> [f32; 4] {
|
||||
// NOTE: a bright gray makes the shadows of the windows look weird.
|
||||
// We use a bit of transparency so that if the user switches on the
|
||||
// `transparent()` option they get immediate results.
|
||||
egui::Color32::from_rgba_unmultiplied(12, 12, 12, 180).into()
|
||||
egui::Color32::from_rgba_unmultiplied(12, 12, 12, 180).to_normalized_gamma_f32()
|
||||
|
||||
// _visuals.window_fill() would also be a natural choice
|
||||
}
|
||||
@@ -176,7 +181,7 @@ pub trait App {
|
||||
}
|
||||
|
||||
/// If `true` a warm-up call to [`Self::update`] will be issued where
|
||||
/// `ctx.memory().everything_is_visible()` will be set to `true`.
|
||||
/// `ctx.memory(|mem| mem.everything_is_visible())` will be set to `true`.
|
||||
///
|
||||
/// This can help pre-caching resources loaded by different parts of the UI, preventing stutter later on.
|
||||
///
|
||||
@@ -258,10 +263,10 @@ pub struct NativeOptions {
|
||||
/// The initial inner size of the native window in points (logical pixels).
|
||||
pub initial_window_size: Option<egui::Vec2>,
|
||||
|
||||
/// The minimum inner window size
|
||||
/// The minimum inner window size in points (logical pixels).
|
||||
pub min_window_size: Option<egui::Vec2>,
|
||||
|
||||
/// The maximum inner window size
|
||||
/// The maximum inner window size in points (logical pixels).
|
||||
pub max_window_size: Option<egui::Vec2>,
|
||||
|
||||
/// Should the app window be resizable?
|
||||
@@ -432,6 +437,7 @@ impl NativeOptions {
|
||||
match dark_light::detect() {
|
||||
dark_light::Mode::Dark => Some(Theme::Dark),
|
||||
dark_light::Mode::Light => Some(Theme::Light),
|
||||
dark_light::Mode::Default => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
@@ -708,6 +714,7 @@ impl Frame {
|
||||
#[doc(alias = "exit")]
|
||||
#[doc(alias = "quit")]
|
||||
pub fn close(&mut self) {
|
||||
tracing::debug!("eframe::Frame::close called");
|
||||
self.output.close = true;
|
||||
}
|
||||
|
||||
@@ -734,6 +741,7 @@ impl Frame {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn set_window_size(&mut self, size: egui::Vec2) {
|
||||
self.output.window_size = Some(size);
|
||||
self.info.window_info.size = size; // so that subsequent calls see the updated value
|
||||
}
|
||||
|
||||
/// Set the desired title of the window.
|
||||
@@ -754,12 +762,14 @@ impl Frame {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn set_fullscreen(&mut self, fullscreen: bool) {
|
||||
self.output.fullscreen = Some(fullscreen);
|
||||
self.info.window_info.fullscreen = fullscreen; // so that subsequent calls see the updated value
|
||||
}
|
||||
|
||||
/// set the position of the outer window.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn set_window_pos(&mut self, pos: egui::Pos2) {
|
||||
self.output.window_pos = Some(pos);
|
||||
self.info.window_info.position = Some(pos); // so that subsequent calls see the updated value
|
||||
}
|
||||
|
||||
/// When called, the native window will follow the
|
||||
|
||||
@@ -103,9 +103,20 @@ pub use web_sys;
|
||||
/// /// You can add more callbacks like this if you want to call in to your code.
|
||||
/// #[cfg(target_arch = "wasm32")]
|
||||
/// #[wasm_bindgen]
|
||||
/// pub async fn start(canvas_id: &str) -> Result<AppRunnerRef>, eframe::wasm_bindgen::JsValue> {
|
||||
/// pub struct WebHandle {
|
||||
/// handle: AppRunnerRef,
|
||||
/// }
|
||||
/// #[cfg(target_arch = "wasm32")]
|
||||
/// #[wasm_bindgen]
|
||||
/// pub async fn start(canvas_id: &str) -> Result<WebHandle, eframe::wasm_bindgen::JsValue> {
|
||||
/// let web_options = eframe::WebOptions::default();
|
||||
/// eframe::start_web(canvas_id, web_options, Box::new(|cc| Box::new(MyEguiApp::new(cc)))).await
|
||||
/// eframe::start_web(
|
||||
/// canvas_id,
|
||||
/// web_options,
|
||||
/// Box::new(|cc| Box::new(MyEguiApp::new(cc))),
|
||||
/// )
|
||||
/// .await
|
||||
/// .map(|handle| WebHandle { handle })
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
@@ -116,7 +127,7 @@ pub async fn start_web(
|
||||
canvas_id: &str,
|
||||
web_options: WebOptions,
|
||||
app_creator: AppCreator,
|
||||
) -> Result<AppRunnerRef, wasm_bindgen::JsValue> {
|
||||
) -> std::result::Result<AppRunnerRef, wasm_bindgen::JsValue> {
|
||||
let handle = web::start(canvas_id, web_options, app_creator).await?;
|
||||
|
||||
Ok(handle)
|
||||
@@ -163,26 +174,63 @@ mod native;
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Errors
|
||||
/// This function can fail if we fail to set up a graphics context.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn run_native(app_name: &str, native_options: NativeOptions, app_creator: AppCreator) {
|
||||
pub fn run_native(
|
||||
app_name: &str,
|
||||
native_options: NativeOptions,
|
||||
app_creator: AppCreator,
|
||||
) -> Result<()> {
|
||||
let renderer = native_options.renderer;
|
||||
|
||||
#[cfg(not(feature = "__screenshot"))]
|
||||
assert!(
|
||||
std::env::var("EFRAME_SCREENSHOT_TO").is_err(),
|
||||
"EFRAME_SCREENSHOT_TO found without compiling with the '__screenshot' feature"
|
||||
);
|
||||
|
||||
match renderer {
|
||||
#[cfg(feature = "glow")]
|
||||
Renderer::Glow => {
|
||||
tracing::debug!("Using the glow renderer");
|
||||
native::run::run_glow(app_name, native_options, app_creator);
|
||||
native::run::run_glow(app_name, native_options, app_creator)
|
||||
}
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
Renderer::Wgpu => {
|
||||
tracing::debug!("Using the wgpu renderer");
|
||||
native::run::run_wgpu(app_name, native_options, app_creator);
|
||||
native::run::run_wgpu(app_name, native_options, app_creator)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// The different problems that can occur when trying to run `eframe`.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[error("winit error: {0}")]
|
||||
Winit(#[from] winit::error::OsError),
|
||||
|
||||
#[cfg(all(feature = "glow", not(target_arch = "wasm32")))]
|
||||
#[error("glutin error: {0}")]
|
||||
Glutin(#[from] glutin::error::Error),
|
||||
|
||||
#[cfg(all(feature = "glow", not(target_arch = "wasm32")))]
|
||||
#[error("Found no glutin configs matching the template: {0:?}")]
|
||||
NoGlutinConfigs(glutin::config::ConfigTemplate),
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[error("WGPU error: {0}")]
|
||||
Wgpu(#[from] egui_wgpu::WgpuError),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Profiling macro for feature "puffin"
|
||||
|
||||
@@ -3,6 +3,11 @@ use winit::event_loop::EventLoopWindowTarget;
|
||||
#[cfg(target_os = "macos")]
|
||||
use winit::platform::macos::WindowBuilderExtMacOS as _;
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
use egui::accesskit;
|
||||
use egui::NumExt as _;
|
||||
#[cfg(feature = "accesskit")]
|
||||
use egui_winit::accesskit_winit;
|
||||
use egui_winit::{native_pixels_per_point, EventResponse, WindowSettings};
|
||||
|
||||
use crate::{epi, Theme, WindowInfo};
|
||||
@@ -44,10 +49,12 @@ pub fn read_window_info(window: &winit::window::Window, pixels_per_point: f32) -
|
||||
}
|
||||
}
|
||||
|
||||
pub fn window_builder(
|
||||
pub fn build_window<E>(
|
||||
event_loop: &EventLoopWindowTarget<E>,
|
||||
title: &str,
|
||||
native_options: &epi::NativeOptions,
|
||||
window_settings: &Option<WindowSettings>,
|
||||
) -> winit::window::WindowBuilder {
|
||||
window_settings: Option<WindowSettings>,
|
||||
) -> Result<winit::window::Window, winit::error::OsError> {
|
||||
let epi::NativeOptions {
|
||||
always_on_top,
|
||||
maximized,
|
||||
@@ -63,19 +70,23 @@ pub fn window_builder(
|
||||
max_window_size,
|
||||
resizable,
|
||||
transparent,
|
||||
centered,
|
||||
..
|
||||
} = native_options;
|
||||
|
||||
let window_icon = icon_data.clone().and_then(load_icon);
|
||||
|
||||
let mut window_builder = winit::window::WindowBuilder::new()
|
||||
.with_always_on_top(*always_on_top)
|
||||
.with_title(title)
|
||||
.with_decorations(*decorated)
|
||||
.with_fullscreen(fullscreen.then(|| winit::window::Fullscreen::Borderless(None)))
|
||||
.with_maximized(*maximized)
|
||||
.with_resizable(*resizable)
|
||||
.with_transparent(*transparent)
|
||||
.with_window_icon(window_icon);
|
||||
.with_window_icon(window_icon)
|
||||
// Keep hidden until we've painted something. See https://github.com/emilk/egui/pull/2279
|
||||
// We must also keep the window hidden until AccessKit is initialized.
|
||||
.with_visible(false);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
if *fullsize_content {
|
||||
@@ -94,21 +105,69 @@ pub fn window_builder(
|
||||
|
||||
window_builder = window_builder_drag_and_drop(window_builder, *drag_and_drop_support);
|
||||
|
||||
if let Some(window_settings) = window_settings {
|
||||
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));
|
||||
window_builder = window_settings.initialize_window(window_builder);
|
||||
window_settings.inner_size_points()
|
||||
} else {
|
||||
if let Some(pos) = *initial_window_pos {
|
||||
window_builder = window_builder.with_position(winit::dpi::PhysicalPosition {
|
||||
window_builder = window_builder.with_position(winit::dpi::LogicalPosition {
|
||||
x: pos.x as f64,
|
||||
y: pos.y as f64,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(initial_window_size) = *initial_window_size {
|
||||
let initial_window_size =
|
||||
initial_window_size.at_most(largest_monitor_point_size(event_loop));
|
||||
window_builder = window_builder.with_inner_size(points_to_size(initial_window_size));
|
||||
}
|
||||
|
||||
*initial_window_size
|
||||
};
|
||||
|
||||
if *centered {
|
||||
if let Some(monitor) = event_loop.available_monitors().next() {
|
||||
let monitor_size = monitor.size();
|
||||
let inner_size = inner_size_points.unwrap_or(egui::Vec2 { x: 800.0, y: 600.0 });
|
||||
if monitor_size.width > 0 && monitor_size.height > 0 {
|
||||
let x = (monitor_size.width - inner_size.x as u32) / 2;
|
||||
let y = (monitor_size.height - inner_size.y as u32) / 2;
|
||||
window_builder = window_builder.with_position(winit::dpi::LogicalPosition {
|
||||
x: x as f64,
|
||||
y: y as f64,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window_builder
|
||||
let window = window_builder.build(event_loop)?;
|
||||
|
||||
use winit::window::WindowLevel;
|
||||
window.set_window_level(if *always_on_top {
|
||||
WindowLevel::AlwaysOnTop
|
||||
} else {
|
||||
WindowLevel::Normal
|
||||
});
|
||||
|
||||
Ok(window)
|
||||
}
|
||||
|
||||
fn largest_monitor_point_size<E>(event_loop: &EventLoopWindowTarget<E>) -> egui::Vec2 {
|
||||
let mut max_size = egui::Vec2::ZERO;
|
||||
|
||||
for monitor in event_loop.available_monitors() {
|
||||
let size = monitor.size().to_logical::<f32>(monitor.scale_factor());
|
||||
let size = egui::vec2(size.width, size.height);
|
||||
max_size = max_size.max(size);
|
||||
}
|
||||
|
||||
if max_size == egui::Vec2::ZERO {
|
||||
egui::Vec2::splat(16000.0)
|
||||
} else {
|
||||
max_size
|
||||
}
|
||||
}
|
||||
|
||||
fn load_icon(icon_data: epi::IconData) -> Option<winit::window::Icon> {
|
||||
@@ -167,7 +226,7 @@ pub fn handle_app_output(
|
||||
}
|
||||
|
||||
if let Some(fullscreen) = fullscreen {
|
||||
window.set_fullscreen(fullscreen.then(|| winit::window::Fullscreen::Borderless(None)));
|
||||
window.set_fullscreen(fullscreen.then_some(winit::window::Fullscreen::Borderless(None)));
|
||||
}
|
||||
|
||||
if let Some(window_title) = window_title {
|
||||
@@ -186,7 +245,12 @@ pub fn handle_app_output(
|
||||
}
|
||||
|
||||
if let Some(always_on_top) = always_on_top {
|
||||
window.set_always_on_top(always_on_top);
|
||||
use winit::window::WindowLevel;
|
||||
window.set_window_level(if always_on_top {
|
||||
WindowLevel::AlwaysOnTop
|
||||
} else {
|
||||
WindowLevel::Normal
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(minimized) = minimized {
|
||||
@@ -235,7 +299,8 @@ impl EpiIntegration {
|
||||
) -> Self {
|
||||
let egui_ctx = egui::Context::default();
|
||||
|
||||
*egui_ctx.memory() = load_egui_memory(storage.as_deref()).unwrap_or_default();
|
||||
let memory = load_egui_memory(storage.as_deref()).unwrap_or_default();
|
||||
egui_ctx.memory_mut(|mem| *mem = memory);
|
||||
|
||||
let native_pixels_per_point = window.scale_factor() as f32;
|
||||
|
||||
@@ -272,13 +337,33 @@ impl EpiIntegration {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub fn init_accesskit<E: From<accesskit_winit::ActionRequestEvent> + Send>(
|
||||
&mut self,
|
||||
window: &winit::window::Window,
|
||||
event_loop_proxy: winit::event_loop::EventLoopProxy<E>,
|
||||
) {
|
||||
let egui_ctx = self.egui_ctx.clone();
|
||||
self.egui_winit
|
||||
.init_accesskit(window, event_loop_proxy, move || {
|
||||
// This function is called when an accessibility client
|
||||
// (e.g. screen reader) makes its first request. If we got here,
|
||||
// we know that an accessibility tree is actually wanted.
|
||||
egui_ctx.enable_accesskit();
|
||||
// Enqueue a repaint so we'll receive a full tree update soon.
|
||||
egui_ctx.request_repaint();
|
||||
egui::accesskit_placeholder_tree_update()
|
||||
});
|
||||
}
|
||||
|
||||
pub fn warm_up(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) {
|
||||
crate::profile_function!();
|
||||
let saved_memory: egui::Memory = self.egui_ctx.memory().clone();
|
||||
self.egui_ctx.memory().set_everything_is_visible(true);
|
||||
let saved_memory: egui::Memory = self.egui_ctx.memory(|mem| mem.clone());
|
||||
self.egui_ctx
|
||||
.memory_mut(|mem| mem.set_everything_is_visible(true));
|
||||
let full_output = self.update(app, window);
|
||||
self.pending_full_output.append(full_output); // Handle it next frame
|
||||
*self.egui_ctx.memory() = saved_memory; // We don't want to remember that windows were huge.
|
||||
self.egui_ctx.memory_mut(|mem| *mem = saved_memory); // We don't want to remember that windows were huge.
|
||||
self.egui_ctx.clear_animations();
|
||||
}
|
||||
|
||||
@@ -295,8 +380,15 @@ impl EpiIntegration {
|
||||
use winit::event::{ElementState, MouseButton, WindowEvent};
|
||||
|
||||
match event {
|
||||
WindowEvent::CloseRequested => self.close = app.on_close_event(),
|
||||
WindowEvent::Destroyed => self.close = true,
|
||||
WindowEvent::CloseRequested => {
|
||||
tracing::debug!("Received WindowEvent::CloseRequested");
|
||||
self.close = app.on_close_event();
|
||||
tracing::debug!("App::on_close_event returned {}", self.close);
|
||||
}
|
||||
WindowEvent::Destroyed => {
|
||||
tracing::debug!("Received WindowEvent::Destroyed");
|
||||
self.close = true;
|
||||
}
|
||||
WindowEvent::MouseInput {
|
||||
button: MouseButton::Left,
|
||||
state: ElementState::Pressed,
|
||||
@@ -311,6 +403,11 @@ impl EpiIntegration {
|
||||
self.egui_winit.on_event(&self.egui_ctx, event)
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub fn on_accesskit_action_request(&mut self, request: accesskit::ActionRequest) {
|
||||
self.egui_winit.on_accesskit_action_request(request);
|
||||
}
|
||||
|
||||
pub fn update(
|
||||
&mut self,
|
||||
app: &mut dyn epi::App,
|
||||
@@ -333,12 +430,13 @@ impl EpiIntegration {
|
||||
self.can_drag_window = false;
|
||||
if app_output.close {
|
||||
self.close = app.on_close_event();
|
||||
tracing::debug!("App::on_close_event returned {}", self.close);
|
||||
}
|
||||
self.frame.output.visible = app_output.visible; // this is handled by post_present
|
||||
handle_app_output(window, self.egui_ctx.pixels_per_point(), app_output);
|
||||
}
|
||||
|
||||
let frame_time = (std::time::Instant::now() - frame_start).as_secs_f64() as f32;
|
||||
let frame_time = frame_start.elapsed().as_secs_f64() as f32;
|
||||
self.frame.info.cpu_usage = Some(frame_time);
|
||||
|
||||
full_output
|
||||
@@ -392,7 +490,8 @@ impl EpiIntegration {
|
||||
}
|
||||
if _app.persist_egui_memory() {
|
||||
crate::profile_scope!("egui_memory");
|
||||
epi::set_value(storage, STORAGE_EGUI_MEMORY_KEY, &*self.egui_ctx.memory());
|
||||
self.egui_ctx
|
||||
.memory(|mem| epi::set_value(storage, STORAGE_EGUI_MEMORY_KEY, mem));
|
||||
}
|
||||
{
|
||||
crate::profile_scope!("App::save");
|
||||
|
||||
@@ -26,6 +26,7 @@ impl FileStorage {
|
||||
/// Store the state in this .ron file.
|
||||
pub fn from_ron_filepath(ron_filepath: impl Into<PathBuf>) -> Self {
|
||||
let ron_filepath: PathBuf = ron_filepath.into();
|
||||
tracing::debug!("Loading app state from {:?}…", ron_filepath);
|
||||
Self {
|
||||
kv: read_ron(&ron_filepath).unwrap_or_default(),
|
||||
ron_filepath,
|
||||
|
||||
@@ -1,19 +1,35 @@
|
||||
//! Note that this file contains two similar paths - one for [`glow`], one for [`wgpu`].
|
||||
//! When making changes to one you often also want to apply it to the other.
|
||||
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use egui_winit::winit;
|
||||
use winit::event_loop::{
|
||||
ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget,
|
||||
};
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
use egui_winit::accesskit_winit;
|
||||
use egui_winit::winit;
|
||||
|
||||
use crate::{epi, Result};
|
||||
|
||||
use super::epi_integration::{self, EpiIntegration};
|
||||
use crate::epi;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RequestRepaintEvent;
|
||||
pub enum UserEvent {
|
||||
RequestRepaint,
|
||||
#[cfg(feature = "accesskit")]
|
||||
AccessKitActionRequest(accesskit_winit::ActionRequestEvent),
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
impl From<accesskit_winit::ActionRequestEvent> for UserEvent {
|
||||
fn from(inner: accesskit_winit::ActionRequestEvent) -> Self {
|
||||
Self::AccessKitActionRequest(inner)
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
@@ -45,14 +61,14 @@ trait WinitApp {
|
||||
fn paint(&mut self) -> EventResult;
|
||||
fn on_event(
|
||||
&mut self,
|
||||
event_loop: &EventLoopWindowTarget<RequestRepaintEvent>,
|
||||
event: &winit::event::Event<'_, RequestRepaintEvent>,
|
||||
) -> EventResult;
|
||||
event_loop: &EventLoopWindowTarget<UserEvent>,
|
||||
event: &winit::event::Event<'_, UserEvent>,
|
||||
) -> Result<EventResult>;
|
||||
}
|
||||
|
||||
fn create_event_loop_builder(
|
||||
native_options: &mut epi::NativeOptions,
|
||||
) -> EventLoopBuilder<RequestRepaintEvent> {
|
||||
) -> EventLoopBuilder<UserEvent> {
|
||||
let mut event_loop_builder = winit::event_loop::EventLoopBuilder::with_user_event();
|
||||
|
||||
if let Some(hook) = std::mem::take(&mut native_options.event_loop_builder) {
|
||||
@@ -66,12 +82,12 @@ fn create_event_loop_builder(
|
||||
///
|
||||
/// We reuse the event-loop so we can support closing and opening an eframe window
|
||||
/// multiple times. This is just a limitation of winit.
|
||||
fn with_event_loop(
|
||||
fn with_event_loop<R>(
|
||||
mut native_options: epi::NativeOptions,
|
||||
f: impl FnOnce(&mut EventLoop<RequestRepaintEvent>, NativeOptions),
|
||||
) {
|
||||
f: impl FnOnce(&mut EventLoop<UserEvent>, NativeOptions) -> R,
|
||||
) -> R {
|
||||
use std::cell::RefCell;
|
||||
thread_local!(static EVENT_LOOP: RefCell<Option<EventLoop<RequestRepaintEvent>>> = RefCell::new(None));
|
||||
thread_local!(static EVENT_LOOP: RefCell<Option<EventLoop<UserEvent>>> = RefCell::new(None));
|
||||
|
||||
EVENT_LOOP.with(|event_loop| {
|
||||
// Since we want to reference NativeOptions when creating the EventLoop we can't
|
||||
@@ -80,22 +96,31 @@ fn with_event_loop(
|
||||
let mut event_loop = event_loop.borrow_mut();
|
||||
let event_loop = event_loop
|
||||
.get_or_insert_with(|| create_event_loop_builder(&mut native_options).build());
|
||||
f(event_loop, native_options);
|
||||
});
|
||||
f(event_loop, native_options)
|
||||
})
|
||||
}
|
||||
|
||||
fn run_and_return(event_loop: &mut EventLoop<RequestRepaintEvent>, mut winit_app: impl WinitApp) {
|
||||
fn run_and_return(
|
||||
event_loop: &mut EventLoop<UserEvent>,
|
||||
mut winit_app: impl WinitApp,
|
||||
) -> Result<()> {
|
||||
use winit::platform::run_return::EventLoopExtRunReturn as _;
|
||||
|
||||
tracing::debug!("event_loop.run_return");
|
||||
tracing::debug!("Entering the winit event loop (run_return)…");
|
||||
|
||||
let mut next_repaint_time = Instant::now();
|
||||
|
||||
let mut returned_result = Ok(());
|
||||
|
||||
event_loop.run_return(|event, event_loop, control_flow| {
|
||||
let event_result = match &event {
|
||||
winit::event::Event::LoopDestroyed => {
|
||||
tracing::debug!("winit::event::Event::LoopDestroyed");
|
||||
EventResult::Exit
|
||||
// On Mac, Cmd-Q we get here and then `run_return` doesn't return (despite its name),
|
||||
// so we need to save state now:
|
||||
tracing::debug!("Received Event::LoopDestroyed - saving app state…");
|
||||
winit_app.save_and_destroy();
|
||||
*control_flow = ControlFlow::Exit;
|
||||
return;
|
||||
}
|
||||
|
||||
// Platform-dependent event handlers to workaround a winit bug
|
||||
@@ -110,7 +135,7 @@ fn run_and_return(event_loop: &mut EventLoop<RequestRepaintEvent>, mut winit_app
|
||||
winit_app.paint()
|
||||
}
|
||||
|
||||
winit::event::Event::UserEvent(RequestRepaintEvent)
|
||||
winit::event::Event::UserEvent(UserEvent::RequestRepaint)
|
||||
| winit::event::Event::NewEvents(winit::event::StartCause::ResumeTimeReached {
|
||||
..
|
||||
}) => EventResult::RepaintNext,
|
||||
@@ -124,15 +149,28 @@ fn run_and_return(event_loop: &mut EventLoop<RequestRepaintEvent>, mut winit_app
|
||||
EventResult::Wait
|
||||
}
|
||||
|
||||
event => winit_app.on_event(event_loop, event),
|
||||
event => match winit_app.on_event(event_loop, event) {
|
||||
Ok(event_result) => event_result,
|
||||
Err(err) => {
|
||||
returned_result = Err(err);
|
||||
tracing::debug!("Exiting because of an error");
|
||||
EventResult::Exit
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
match event_result {
|
||||
EventResult::Wait => {}
|
||||
EventResult::RepaintNow => {
|
||||
tracing::trace!("Repaint caused by winit::Event: {:?}", event);
|
||||
next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000);
|
||||
winit_app.paint();
|
||||
if cfg!(windows) {
|
||||
// Fix flickering on Windows, see https://github.com/emilk/egui/pull/2280
|
||||
next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000);
|
||||
winit_app.paint();
|
||||
} else {
|
||||
// Fix for https://github.com/emilk/egui/issues/2425
|
||||
next_repaint_time = Instant::now();
|
||||
}
|
||||
}
|
||||
EventResult::RepaintNext => {
|
||||
tracing::trace!("Repaint caused by winit::Event: {:?}", event);
|
||||
@@ -142,10 +180,7 @@ fn run_and_return(event_loop: &mut EventLoop<RequestRepaintEvent>, mut winit_app
|
||||
next_repaint_time = next_repaint_time.min(repaint_time);
|
||||
}
|
||||
EventResult::Exit => {
|
||||
// On Cmd-Q we get here and then `run_return` doesn't return,
|
||||
// so we need to save state now:
|
||||
tracing::debug!("Exiting event loop - saving app state…");
|
||||
winit_app.save_and_destroy();
|
||||
tracing::debug!("Asking to exit event loop…");
|
||||
*control_flow = ControlFlow::Exit;
|
||||
return;
|
||||
}
|
||||
@@ -174,19 +209,21 @@ fn run_and_return(event_loop: &mut EventLoop<RequestRepaintEvent>, mut winit_app
|
||||
event_loop.run_return(|_, _, control_flow| {
|
||||
control_flow.set_exit();
|
||||
});
|
||||
|
||||
returned_result
|
||||
}
|
||||
|
||||
fn run_and_exit(
|
||||
event_loop: EventLoop<RequestRepaintEvent>,
|
||||
mut winit_app: impl WinitApp + 'static,
|
||||
) -> ! {
|
||||
tracing::debug!("event_loop.run");
|
||||
fn run_and_exit(event_loop: EventLoop<UserEvent>, mut winit_app: impl WinitApp + 'static) -> ! {
|
||||
tracing::debug!("Entering the winit event loop (run)…");
|
||||
|
||||
let mut next_repaint_time = Instant::now();
|
||||
|
||||
event_loop.run(move |event, event_loop, control_flow| {
|
||||
let event_result = match event {
|
||||
winit::event::Event::LoopDestroyed => EventResult::Exit,
|
||||
winit::event::Event::LoopDestroyed => {
|
||||
tracing::debug!("Received Event::LoopDestroyed");
|
||||
EventResult::Exit
|
||||
}
|
||||
|
||||
// Platform-dependent event handlers to workaround a winit bug
|
||||
// See: https://github.com/rust-windowing/winit/issues/987
|
||||
@@ -200,19 +237,30 @@ fn run_and_exit(
|
||||
winit_app.paint()
|
||||
}
|
||||
|
||||
winit::event::Event::UserEvent(RequestRepaintEvent)
|
||||
winit::event::Event::UserEvent(UserEvent::RequestRepaint)
|
||||
| winit::event::Event::NewEvents(winit::event::StartCause::ResumeTimeReached {
|
||||
..
|
||||
}) => EventResult::RepaintNext,
|
||||
|
||||
event => winit_app.on_event(event_loop, &event),
|
||||
event => match winit_app.on_event(event_loop, &event) {
|
||||
Ok(event_result) => event_result,
|
||||
Err(err) => {
|
||||
panic!("eframe encountered a fatal error: {err}");
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
match event_result {
|
||||
EventResult::Wait => {}
|
||||
EventResult::RepaintNow => {
|
||||
next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000);
|
||||
winit_app.paint();
|
||||
if cfg!(windows) {
|
||||
// Fix flickering on Windows, see https://github.com/emilk/egui/pull/2280
|
||||
next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000);
|
||||
winit_app.paint();
|
||||
} else {
|
||||
// Fix for https://github.com/emilk/egui/issues/2425
|
||||
next_repaint_time = Instant::now();
|
||||
}
|
||||
}
|
||||
EventResult::RepaintNext => {
|
||||
next_repaint_time = Instant::now();
|
||||
@@ -221,7 +269,7 @@ fn run_and_exit(
|
||||
next_repaint_time = next_repaint_time.min(repaint_time);
|
||||
}
|
||||
EventResult::Exit => {
|
||||
tracing::debug!("Quitting…");
|
||||
tracing::debug!("Quitting - saving app state…");
|
||||
winit_app.save_and_destroy();
|
||||
#[allow(clippy::exit)]
|
||||
std::process::exit(0);
|
||||
@@ -242,33 +290,14 @@ fn run_and_exit(
|
||||
})
|
||||
}
|
||||
|
||||
fn centere_window_pos(
|
||||
monitor: Option<winit::monitor::MonitorHandle>,
|
||||
native_options: &mut epi::NativeOptions,
|
||||
) {
|
||||
// Get the current_monitor.
|
||||
if let Some(monitor) = monitor {
|
||||
let monitor_size = monitor.size();
|
||||
let inner_size = native_options
|
||||
.initial_window_size
|
||||
.unwrap_or(egui::Vec2 { x: 800.0, y: 600.0 });
|
||||
if monitor_size.width > 0 && monitor_size.height > 0 {
|
||||
let x = (monitor_size.width - inner_size.x as u32) / 2;
|
||||
let y = (monitor_size.height - inner_size.y as u32) / 2;
|
||||
native_options.initial_window_pos = Some(egui::Pos2 {
|
||||
x: x as _,
|
||||
y: y as _,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
/// Run an egui app
|
||||
#[cfg(feature = "glow")]
|
||||
mod glow_integration {
|
||||
use std::sync::Arc;
|
||||
|
||||
use egui::NumExt as _;
|
||||
|
||||
use super::*;
|
||||
|
||||
// Note: that the current Glutin API design tightly couples the GL context with
|
||||
@@ -295,11 +324,134 @@ mod glow_integration {
|
||||
|
||||
// Conceptually this will be split out eventually so that the rest of the state
|
||||
// can be persistent.
|
||||
gl_window: glutin::WindowedContext<glutin::PossiblyCurrent>,
|
||||
gl_window: GlutinWindowContext,
|
||||
}
|
||||
struct GlutinWindowContext {
|
||||
window: winit::window::Window,
|
||||
gl_context: glutin::context::PossiblyCurrentContext,
|
||||
gl_display: glutin::display::Display,
|
||||
gl_surface: glutin::surface::Surface<glutin::surface::WindowSurface>,
|
||||
}
|
||||
|
||||
impl GlutinWindowContext {
|
||||
// refactor this function to use `glutin-winit` crate eventually.
|
||||
// preferably add android support at the same time.
|
||||
#[allow(unsafe_code)]
|
||||
unsafe fn new(
|
||||
winit_window: winit::window::Window,
|
||||
native_options: &epi::NativeOptions,
|
||||
) -> Result<Self> {
|
||||
use glutin::prelude::*;
|
||||
use raw_window_handle::*;
|
||||
let hardware_acceleration = match native_options.hardware_acceleration {
|
||||
crate::HardwareAcceleration::Required => Some(true),
|
||||
crate::HardwareAcceleration::Preferred => None,
|
||||
crate::HardwareAcceleration::Off => Some(false),
|
||||
};
|
||||
|
||||
let raw_display_handle = winit_window.raw_display_handle();
|
||||
let raw_window_handle = winit_window.raw_window_handle();
|
||||
|
||||
// EGL is crossplatform and the official khronos way
|
||||
// but sometimes platforms/drivers may not have it, so we use back up options where possible.
|
||||
// TODO: check whether we can expose these options as "features", so that users can select the relevant backend they want.
|
||||
|
||||
// try egl and fallback to windows wgl. Windows is the only platform that *requires* window handle to create display.
|
||||
#[cfg(target_os = "windows")]
|
||||
let preference =
|
||||
glutin::display::DisplayApiPreference::EglThenWgl(Some(raw_window_handle));
|
||||
// try egl and fallback to x11 glx
|
||||
#[cfg(target_os = "linux")]
|
||||
let preference = glutin::display::DisplayApiPreference::EglThenGlx(Box::new(
|
||||
winit::platform::x11::register_xlib_error_hook,
|
||||
));
|
||||
#[cfg(target_os = "macos")]
|
||||
let preference = glutin::display::DisplayApiPreference::Cgl;
|
||||
#[cfg(target_os = "android")]
|
||||
let preference = glutin::display::DisplayApiPreference::Egl;
|
||||
|
||||
let gl_display = glutin::display::Display::new(raw_display_handle, preference)?;
|
||||
let swap_interval = if native_options.vsync {
|
||||
glutin::surface::SwapInterval::Wait(std::num::NonZeroU32::new(1).unwrap())
|
||||
} else {
|
||||
glutin::surface::SwapInterval::DontWait
|
||||
};
|
||||
|
||||
let config_template = glutin::config::ConfigTemplateBuilder::new()
|
||||
.prefer_hardware_accelerated(hardware_acceleration)
|
||||
.with_depth_size(native_options.depth_buffer);
|
||||
// we don't know if multi sampling option is set. so, check if its more than 0.
|
||||
let config_template = if native_options.multisampling > 0 {
|
||||
config_template.with_multisampling(
|
||||
native_options
|
||||
.multisampling
|
||||
.try_into()
|
||||
.expect("failed to fit multisamples into u8"),
|
||||
)
|
||||
} else {
|
||||
config_template
|
||||
};
|
||||
let config_template = config_template
|
||||
.with_stencil_size(native_options.stencil_buffer)
|
||||
.with_transparency(native_options.transparent)
|
||||
.compatible_with_native_window(raw_window_handle)
|
||||
.build();
|
||||
// finds all valid configurations supported by this display that match the config_template
|
||||
// this is where we will try to get a "fallback" config if we are okay with ignoring some native
|
||||
// options required by user like multi sampling, srgb, transparency etc..
|
||||
// TODO: need to figure out a good fallback config template
|
||||
let config = gl_display
|
||||
.find_configs(config_template.clone())?
|
||||
.next()
|
||||
.ok_or(crate::Error::NoGlutinConfigs(config_template))?;
|
||||
|
||||
let context_attributes =
|
||||
glutin::context::ContextAttributesBuilder::new().build(Some(raw_window_handle));
|
||||
// for surface creation.
|
||||
let (width, height): (u32, u32) = winit_window.inner_size().into();
|
||||
let width = std::num::NonZeroU32::new(width.at_least(1)).unwrap();
|
||||
let height = std::num::NonZeroU32::new(height.at_least(1)).unwrap();
|
||||
let surface_attributes =
|
||||
glutin::surface::SurfaceAttributesBuilder::<glutin::surface::WindowSurface>::new()
|
||||
.build(raw_window_handle, width, height);
|
||||
// start creating the gl objects
|
||||
let gl_context = gl_display.create_context(&config, &context_attributes)?;
|
||||
|
||||
let gl_surface = gl_display.create_window_surface(&config, &surface_attributes)?;
|
||||
let gl_context = gl_context.make_current(&gl_surface)?;
|
||||
gl_surface.set_swap_interval(&gl_context, swap_interval)?;
|
||||
Ok(GlutinWindowContext {
|
||||
window: winit_window,
|
||||
gl_context,
|
||||
gl_display,
|
||||
gl_surface,
|
||||
})
|
||||
}
|
||||
|
||||
fn window(&self) -> &winit::window::Window {
|
||||
&self.window
|
||||
}
|
||||
|
||||
fn resize(&self, physical_size: winit::dpi::PhysicalSize<u32>) {
|
||||
use glutin::surface::GlSurface;
|
||||
let width = std::num::NonZeroU32::new(physical_size.width.at_least(1)).unwrap();
|
||||
let height = std::num::NonZeroU32::new(physical_size.height.at_least(1)).unwrap();
|
||||
self.gl_surface.resize(&self.gl_context, width, height);
|
||||
}
|
||||
|
||||
fn swap_buffers(&self) -> glutin::error::Result<()> {
|
||||
use glutin::surface::GlSurface;
|
||||
self.gl_surface.swap_buffers(&self.gl_context)
|
||||
}
|
||||
|
||||
fn get_proc_address(&self, addr: &std::ffi::CStr) -> *const std::ffi::c_void {
|
||||
use glutin::display::GlDisplay;
|
||||
self.gl_display.get_proc_address(addr)
|
||||
}
|
||||
}
|
||||
|
||||
struct GlowWinitApp {
|
||||
repaint_proxy: Arc<egui::mutex::Mutex<EventLoopProxy<RequestRepaintEvent>>>,
|
||||
repaint_proxy: Arc<egui::mutex::Mutex<EventLoopProxy<UserEvent>>>,
|
||||
app_name: String,
|
||||
native_options: epi::NativeOptions,
|
||||
running: Option<GlowWinitRunning>,
|
||||
@@ -309,11 +461,13 @@ mod glow_integration {
|
||||
// suspends and resumes.
|
||||
app_creator: Option<epi::AppCreator>,
|
||||
is_focused: bool,
|
||||
|
||||
frame_nr: u64,
|
||||
}
|
||||
|
||||
impl GlowWinitApp {
|
||||
fn new(
|
||||
event_loop: &EventLoop<RequestRepaintEvent>,
|
||||
event_loop: &EventLoop<UserEvent>,
|
||||
app_name: &str,
|
||||
native_options: epi::NativeOptions,
|
||||
app_creator: epi::AppCreator,
|
||||
@@ -325,54 +479,39 @@ mod glow_integration {
|
||||
running: None,
|
||||
app_creator: Some(app_creator),
|
||||
is_focused: true,
|
||||
frame_nr: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
fn create_glutin_windowed_context(
|
||||
event_loop: &EventLoopWindowTarget<RequestRepaintEvent>,
|
||||
event_loop: &EventLoopWindowTarget<UserEvent>,
|
||||
storage: Option<&dyn epi::Storage>,
|
||||
title: &String,
|
||||
title: &str,
|
||||
native_options: &NativeOptions,
|
||||
) -> (
|
||||
glutin::WindowedContext<glutin::PossiblyCurrent>,
|
||||
glow::Context,
|
||||
) {
|
||||
) -> Result<(GlutinWindowContext, glow::Context)> {
|
||||
crate::profile_function!();
|
||||
|
||||
use crate::HardwareAcceleration;
|
||||
|
||||
let hardware_acceleration = match native_options.hardware_acceleration {
|
||||
HardwareAcceleration::Required => Some(true),
|
||||
HardwareAcceleration::Preferred => None,
|
||||
HardwareAcceleration::Off => Some(false),
|
||||
};
|
||||
let window_settings = epi_integration::load_window_settings(storage);
|
||||
|
||||
let window_builder = epi_integration::window_builder(native_options, &window_settings)
|
||||
.with_title(title)
|
||||
.with_visible(false); // Keep hidden until we've painted something. See https://github.com/emilk/egui/pull/2279
|
||||
let winit_window =
|
||||
epi_integration::build_window(event_loop, title, native_options, window_settings)?;
|
||||
// a lot of the code below has been lifted from glutin example in their repo.
|
||||
let glutin_window_context =
|
||||
unsafe { GlutinWindowContext::new(winit_window, native_options)? };
|
||||
let gl = unsafe {
|
||||
glow::Context::from_loader_function(|s| {
|
||||
let s = std::ffi::CString::new(s)
|
||||
.expect("failed to construct C string from string for gl proc address");
|
||||
|
||||
let gl_window = unsafe {
|
||||
glutin::ContextBuilder::new()
|
||||
.with_hardware_acceleration(hardware_acceleration)
|
||||
.with_depth_buffer(native_options.depth_buffer)
|
||||
.with_multisampling(native_options.multisampling)
|
||||
.with_stencil_buffer(native_options.stencil_buffer)
|
||||
.with_vsync(native_options.vsync)
|
||||
.build_windowed(window_builder, event_loop)
|
||||
.unwrap()
|
||||
.make_current()
|
||||
.unwrap()
|
||||
glutin_window_context.get_proc_address(&s)
|
||||
})
|
||||
};
|
||||
|
||||
let gl =
|
||||
unsafe { glow::Context::from_loader_function(|s| gl_window.get_proc_address(s)) };
|
||||
|
||||
(gl_window, gl)
|
||||
Ok((glutin_window_context, gl))
|
||||
}
|
||||
|
||||
fn init_run_state(&mut self, event_loop: &EventLoopWindowTarget<RequestRepaintEvent>) {
|
||||
fn init_run_state(&mut self, event_loop: &EventLoopWindowTarget<UserEvent>) -> Result<()> {
|
||||
let storage = epi_integration::create_storage(&self.app_name);
|
||||
|
||||
let (gl_window, gl) = Self::create_glutin_windowed_context(
|
||||
@@ -380,7 +519,7 @@ mod glow_integration {
|
||||
storage.as_deref(),
|
||||
&self.app_name,
|
||||
&self.native_options,
|
||||
);
|
||||
)?;
|
||||
let gl = Arc::new(gl);
|
||||
|
||||
let painter =
|
||||
@@ -398,6 +537,10 @@ mod glow_integration {
|
||||
#[cfg(feature = "wgpu")]
|
||||
None,
|
||||
);
|
||||
#[cfg(feature = "accesskit")]
|
||||
{
|
||||
integration.init_accesskit(gl_window.window(), self.repaint_proxy.lock().clone());
|
||||
}
|
||||
let theme = system_theme.unwrap_or(self.native_options.default_theme);
|
||||
integration.egui_ctx.set_visuals(theme.egui_visuals());
|
||||
|
||||
@@ -409,7 +552,10 @@ mod glow_integration {
|
||||
{
|
||||
let event_loop_proxy = self.repaint_proxy.clone();
|
||||
integration.egui_ctx.set_request_repaint_callback(move || {
|
||||
event_loop_proxy.lock().send_event(RequestRepaintEvent).ok();
|
||||
event_loop_proxy
|
||||
.lock()
|
||||
.send_event(UserEvent::RequestRepaint)
|
||||
.ok();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -435,6 +581,8 @@ mod glow_integration {
|
||||
integration,
|
||||
app,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -515,6 +663,26 @@ mod glow_integration {
|
||||
|
||||
integration.post_present(window);
|
||||
|
||||
#[cfg(feature = "__screenshot")]
|
||||
// give it time to settle:
|
||||
if self.frame_nr == 2 {
|
||||
if let Ok(path) = std::env::var("EFRAME_SCREENSHOT_TO") {
|
||||
assert!(
|
||||
path.ends_with(".png"),
|
||||
"Expected EFRAME_SCREENSHOT_TO to end with '.png', got {path:?}"
|
||||
);
|
||||
let [w, h] = screen_size_in_pixels;
|
||||
let pixels = painter.read_screen_rgba(screen_size_in_pixels);
|
||||
let image = image::RgbaImage::from_vec(w, h, pixels).unwrap();
|
||||
let image = image::imageops::flip_vertical(&image);
|
||||
image.save(&path).unwrap_or_else(|err| {
|
||||
panic!("Failed to save screenshot to {path:?}: {err}");
|
||||
});
|
||||
eprintln!("Screenshot saved to {path:?}.");
|
||||
std::process::exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
let control_flow = if integration.should_close() {
|
||||
EventResult::Exit
|
||||
} else if repaint_after.is_zero() {
|
||||
@@ -544,6 +712,8 @@ mod glow_integration {
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
|
||||
self.frame_nr += 1;
|
||||
|
||||
control_flow
|
||||
} else {
|
||||
EventResult::Wait
|
||||
@@ -552,13 +722,13 @@ mod glow_integration {
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
event_loop: &EventLoopWindowTarget<RequestRepaintEvent>,
|
||||
event: &winit::event::Event<'_, RequestRepaintEvent>,
|
||||
) -> EventResult {
|
||||
match event {
|
||||
event_loop: &EventLoopWindowTarget<UserEvent>,
|
||||
event: &winit::event::Event<'_, UserEvent>,
|
||||
) -> Result<EventResult> {
|
||||
Ok(match event {
|
||||
winit::event::Event::Resumed => {
|
||||
if self.running.is_none() {
|
||||
self.init_run_state(event_loop);
|
||||
self.init_run_state(event_loop)?;
|
||||
}
|
||||
EventResult::RepaintNow
|
||||
}
|
||||
@@ -621,7 +791,8 @@ mod glow_integration {
|
||||
winit::event::WindowEvent::CloseRequested
|
||||
if running.integration.should_close() =>
|
||||
{
|
||||
return EventResult::Exit
|
||||
tracing::debug!("Received WindowEvent::CloseRequested");
|
||||
return Ok(EventResult::Exit);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -644,8 +815,23 @@ mod glow_integration {
|
||||
EventResult::Wait
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "accesskit")]
|
||||
winit::event::Event::UserEvent(UserEvent::AccessKitActionRequest(
|
||||
accesskit_winit::ActionRequestEvent { request, .. },
|
||||
)) => {
|
||||
if let Some(running) = &mut self.running {
|
||||
running
|
||||
.integration
|
||||
.on_accesskit_action_request(request.clone());
|
||||
// As a form of user input, accessibility actions should
|
||||
// lead to a repaint.
|
||||
EventResult::RepaintNext
|
||||
} else {
|
||||
EventResult::Wait
|
||||
}
|
||||
}
|
||||
_ => EventResult::Wait,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -653,24 +839,15 @@ mod glow_integration {
|
||||
app_name: &str,
|
||||
mut native_options: epi::NativeOptions,
|
||||
app_creator: epi::AppCreator,
|
||||
) {
|
||||
) -> Result<()> {
|
||||
if native_options.run_and_return {
|
||||
with_event_loop(native_options, |event_loop, mut native_options| {
|
||||
if native_options.centered {
|
||||
centere_window_pos(event_loop.available_monitors().next(), &mut native_options);
|
||||
}
|
||||
|
||||
with_event_loop(native_options, |event_loop, native_options| {
|
||||
let glow_eframe =
|
||||
GlowWinitApp::new(event_loop, app_name, native_options, app_creator);
|
||||
run_and_return(event_loop, glow_eframe);
|
||||
});
|
||||
run_and_return(event_loop, glow_eframe)
|
||||
})
|
||||
} else {
|
||||
let event_loop = create_event_loop_builder(&mut native_options).build();
|
||||
|
||||
if native_options.centered {
|
||||
centere_window_pos(event_loop.available_monitors().next(), &mut native_options);
|
||||
}
|
||||
|
||||
let glow_eframe = GlowWinitApp::new(&event_loop, app_name, native_options, app_creator);
|
||||
run_and_exit(event_loop, glow_eframe);
|
||||
}
|
||||
@@ -697,7 +874,7 @@ mod wgpu_integration {
|
||||
}
|
||||
|
||||
struct WgpuWinitApp {
|
||||
repaint_proxy: Arc<std::sync::Mutex<EventLoopProxy<RequestRepaintEvent>>>,
|
||||
repaint_proxy: Arc<std::sync::Mutex<EventLoopProxy<UserEvent>>>,
|
||||
app_name: String,
|
||||
native_options: epi::NativeOptions,
|
||||
app_creator: Option<epi::AppCreator>,
|
||||
@@ -711,11 +888,17 @@ mod wgpu_integration {
|
||||
|
||||
impl WgpuWinitApp {
|
||||
fn new(
|
||||
event_loop: &EventLoop<RequestRepaintEvent>,
|
||||
event_loop: &EventLoop<UserEvent>,
|
||||
app_name: &str,
|
||||
native_options: epi::NativeOptions,
|
||||
app_creator: epi::AppCreator,
|
||||
) -> Self {
|
||||
#[cfg(feature = "__screenshot")]
|
||||
assert!(
|
||||
std::env::var("EFRAME_SCREENSHOT_TO").is_err(),
|
||||
"EFRAME_SCREENSHOT_TO not yet implemented for wgpu backend"
|
||||
);
|
||||
|
||||
Self {
|
||||
repaint_proxy: Arc::new(std::sync::Mutex::new(event_loop.create_proxy())),
|
||||
app_name: app_name.to_owned(),
|
||||
@@ -728,46 +911,47 @@ mod wgpu_integration {
|
||||
}
|
||||
|
||||
fn create_window(
|
||||
event_loop: &EventLoopWindowTarget<RequestRepaintEvent>,
|
||||
event_loop: &EventLoopWindowTarget<UserEvent>,
|
||||
storage: Option<&dyn epi::Storage>,
|
||||
title: &String,
|
||||
title: &str,
|
||||
native_options: &NativeOptions,
|
||||
) -> winit::window::Window {
|
||||
) -> std::result::Result<winit::window::Window, winit::error::OsError> {
|
||||
let window_settings = epi_integration::load_window_settings(storage);
|
||||
epi_integration::window_builder(native_options, &window_settings)
|
||||
.with_title(title)
|
||||
.with_visible(false) // Keep hidden until we've painted something. See https://github.com/emilk/egui/pull/2279
|
||||
.build(event_loop)
|
||||
.unwrap()
|
||||
epi_integration::build_window(event_loop, title, native_options, window_settings)
|
||||
}
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
fn set_window(&mut self, window: winit::window::Window) {
|
||||
fn set_window(
|
||||
&mut self,
|
||||
window: winit::window::Window,
|
||||
) -> std::result::Result<(), egui_wgpu::WgpuError> {
|
||||
self.window = Some(window);
|
||||
if let Some(running) = &mut self.running {
|
||||
unsafe {
|
||||
running.painter.set_window(self.window.as_ref());
|
||||
pollster::block_on(running.painter.set_window(self.window.as_ref()))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
#[cfg(target_os = "android")]
|
||||
fn drop_window(&mut self) {
|
||||
fn drop_window(&mut self) -> std::result::Result<(), egui_wgpu::WgpuError> {
|
||||
self.window = None;
|
||||
if let Some(running) = &mut self.running {
|
||||
unsafe {
|
||||
running.painter.set_window(None);
|
||||
pollster::block_on(running.painter.set_window(None))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn init_run_state(
|
||||
&mut self,
|
||||
event_loop: &EventLoopWindowTarget<RequestRepaintEvent>,
|
||||
event_loop: &EventLoopWindowTarget<UserEvent>,
|
||||
storage: Option<Box<dyn epi::Storage>>,
|
||||
window: winit::window::Window,
|
||||
) {
|
||||
) -> std::result::Result<(), egui_wgpu::WgpuError> {
|
||||
#[allow(unsafe_code, unused_mut, unused_unsafe)]
|
||||
let painter = unsafe {
|
||||
let mut painter = egui_wgpu::winit::Painter::new(
|
||||
@@ -775,7 +959,7 @@ mod wgpu_integration {
|
||||
self.native_options.multisampling.max(1) as _,
|
||||
self.native_options.depth_buffer,
|
||||
);
|
||||
painter.set_window(Some(&window));
|
||||
pollster::block_on(painter.set_window(Some(&window)))?;
|
||||
painter
|
||||
};
|
||||
|
||||
@@ -792,6 +976,10 @@ mod wgpu_integration {
|
||||
None,
|
||||
wgpu_render_state.clone(),
|
||||
);
|
||||
#[cfg(feature = "accesskit")]
|
||||
{
|
||||
integration.init_accesskit(&window, self.repaint_proxy.lock().unwrap().clone());
|
||||
}
|
||||
let theme = system_theme.unwrap_or(self.native_options.default_theme);
|
||||
integration.egui_ctx.set_visuals(theme.egui_visuals());
|
||||
|
||||
@@ -803,7 +991,7 @@ mod wgpu_integration {
|
||||
event_loop_proxy
|
||||
.lock()
|
||||
.unwrap()
|
||||
.send_event(RequestRepaintEvent)
|
||||
.send_event(UserEvent::RequestRepaint)
|
||||
.ok();
|
||||
});
|
||||
}
|
||||
@@ -829,6 +1017,8 @@ mod wgpu_integration {
|
||||
app,
|
||||
});
|
||||
self.window = Some(window);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -934,10 +1124,10 @@ mod wgpu_integration {
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
event_loop: &EventLoopWindowTarget<RequestRepaintEvent>,
|
||||
event: &winit::event::Event<'_, RequestRepaintEvent>,
|
||||
) -> EventResult {
|
||||
match event {
|
||||
event_loop: &EventLoopWindowTarget<UserEvent>,
|
||||
event: &winit::event::Event<'_, UserEvent>,
|
||||
) -> Result<EventResult> {
|
||||
Ok(match event {
|
||||
winit::event::Event::Resumed => {
|
||||
if let Some(running) = &self.running {
|
||||
if self.window.is_none() {
|
||||
@@ -946,8 +1136,8 @@ mod wgpu_integration {
|
||||
running.integration.frame.storage(),
|
||||
&self.app_name,
|
||||
&self.native_options,
|
||||
);
|
||||
self.set_window(window);
|
||||
)?;
|
||||
self.set_window(window)?;
|
||||
}
|
||||
} else {
|
||||
let storage = epi_integration::create_storage(&self.app_name);
|
||||
@@ -956,14 +1146,14 @@ mod wgpu_integration {
|
||||
storage.as_deref(),
|
||||
&self.app_name,
|
||||
&self.native_options,
|
||||
);
|
||||
self.init_run_state(event_loop, storage, window);
|
||||
)?;
|
||||
self.init_run_state(event_loop, storage, window)?;
|
||||
}
|
||||
EventResult::RepaintNow
|
||||
}
|
||||
winit::event::Event::Suspended => {
|
||||
#[cfg(target_os = "android")]
|
||||
self.drop_window();
|
||||
self.drop_window()?;
|
||||
EventResult::Wait
|
||||
}
|
||||
|
||||
@@ -1013,7 +1203,8 @@ mod wgpu_integration {
|
||||
winit::event::WindowEvent::CloseRequested
|
||||
if running.integration.should_close() =>
|
||||
{
|
||||
return EventResult::Exit
|
||||
tracing::debug!("Received WindowEvent::CloseRequested");
|
||||
return Ok(EventResult::Exit);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
@@ -1035,8 +1226,23 @@ mod wgpu_integration {
|
||||
EventResult::Wait
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "accesskit")]
|
||||
winit::event::Event::UserEvent(UserEvent::AccessKitActionRequest(
|
||||
accesskit_winit::ActionRequestEvent { request, .. },
|
||||
)) => {
|
||||
if let Some(running) = &mut self.running {
|
||||
running
|
||||
.integration
|
||||
.on_accesskit_action_request(request.clone());
|
||||
// As a form of user input, accessibility actions should
|
||||
// lead to a repaint.
|
||||
EventResult::RepaintNext
|
||||
} else {
|
||||
EventResult::Wait
|
||||
}
|
||||
}
|
||||
_ => EventResult::Wait,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1044,24 +1250,15 @@ mod wgpu_integration {
|
||||
app_name: &str,
|
||||
mut native_options: epi::NativeOptions,
|
||||
app_creator: epi::AppCreator,
|
||||
) {
|
||||
) -> Result<()> {
|
||||
if native_options.run_and_return {
|
||||
with_event_loop(native_options, |event_loop, mut native_options| {
|
||||
if native_options.centered {
|
||||
centere_window_pos(event_loop.available_monitors().next(), &mut native_options);
|
||||
}
|
||||
|
||||
with_event_loop(native_options, |event_loop, native_options| {
|
||||
let wgpu_eframe =
|
||||
WgpuWinitApp::new(event_loop, app_name, native_options, app_creator);
|
||||
run_and_return(event_loop, wgpu_eframe);
|
||||
});
|
||||
run_and_return(event_loop, wgpu_eframe)
|
||||
})
|
||||
} else {
|
||||
let event_loop = create_event_loop_builder(&mut native_options).build();
|
||||
|
||||
if native_options.centered {
|
||||
centere_window_pos(event_loop.available_monitors().next(), &mut native_options);
|
||||
}
|
||||
|
||||
let wgpu_eframe = WgpuWinitApp::new(&event_loop, app_name, native_options, app_creator);
|
||||
run_and_exit(event_loop, wgpu_eframe);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use super::{web_painter::WebPainter, *};
|
||||
use crate::epi;
|
||||
|
||||
use egui::{
|
||||
mutex::{Mutex, MutexGuard},
|
||||
TexturesDelta,
|
||||
};
|
||||
pub use egui::{pos2, Color32};
|
||||
|
||||
use crate::{epi, App};
|
||||
|
||||
use super::{web_painter::WebPainter, *};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
@@ -284,7 +284,7 @@ impl AppRunner {
|
||||
/// Get mutable access to the concrete [`App`] we enclose.
|
||||
///
|
||||
/// This will panic if your app does not implement [`App::as_any_mut`].
|
||||
pub fn app_mut<ConreteApp: 'static + crate::App>(&mut self) -> &mut ConreteApp {
|
||||
pub fn app_mut<ConreteApp: 'static + App>(&mut self) -> &mut ConreteApp {
|
||||
self.app
|
||||
.as_any_mut()
|
||||
.expect("Your app must implement `as_any_mut`, but it doesn't")
|
||||
@@ -313,10 +313,11 @@ impl AppRunner {
|
||||
|
||||
pub fn warm_up(&mut self) -> Result<(), JsValue> {
|
||||
if self.app.warm_up_enabled() {
|
||||
let saved_memory: egui::Memory = self.egui_ctx.memory().clone();
|
||||
self.egui_ctx.memory().set_everything_is_visible(true);
|
||||
let saved_memory: egui::Memory = self.egui_ctx.memory(|m| m.clone());
|
||||
self.egui_ctx
|
||||
.memory_mut(|m| m.set_everything_is_visible(true));
|
||||
self.logic()?;
|
||||
*self.egui_ctx.memory() = saved_memory; // We don't want to remember that windows were huge.
|
||||
self.egui_ctx.memory_mut(|m| *m = saved_memory); // We don't want to remember that windows were huge.
|
||||
self.egui_ctx.clear_animations();
|
||||
}
|
||||
Ok(())
|
||||
@@ -388,7 +389,7 @@ impl AppRunner {
|
||||
}
|
||||
|
||||
fn handle_platform_output(&mut self, platform_output: egui::PlatformOutput) {
|
||||
if self.egui_ctx.options().screen_reader {
|
||||
if self.egui_ctx.options(|o| o.screen_reader) {
|
||||
self.screen_reader
|
||||
.speak(&platform_output.events_description());
|
||||
}
|
||||
@@ -400,6 +401,8 @@ impl AppRunner {
|
||||
events: _, // already handled
|
||||
mutable_text_under_cursor,
|
||||
text_cursor_pos,
|
||||
#[cfg(feature = "accesskit")]
|
||||
accesskit_update: _, // not currently implemented
|
||||
} = platform_output;
|
||||
|
||||
set_cursor_icon(cursor_icon);
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use super::*;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use egui::Key;
|
||||
|
||||
use super::*;
|
||||
|
||||
struct IsDestroyed(pub bool);
|
||||
|
||||
pub fn paint_and_schedule(
|
||||
@@ -64,11 +67,13 @@ pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Res
|
||||
runner_lock.input.raw.modifiers = modifiers;
|
||||
|
||||
let key = event.key();
|
||||
let egui_key = translate_key(&key);
|
||||
|
||||
if let Some(key) = translate_key(&key) {
|
||||
if let Some(key) = egui_key {
|
||||
runner_lock.input.raw.events.push(egui::Event::Key {
|
||||
key,
|
||||
pressed: true,
|
||||
repeat: false, // egui will fill this in for us!
|
||||
modifiers,
|
||||
});
|
||||
}
|
||||
@@ -84,10 +89,13 @@ pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Res
|
||||
|
||||
let egui_wants_keyboard = runner_lock.egui_ctx().wants_keyboard_input();
|
||||
|
||||
let prevent_default = if matches!(event.key().as_str(), "Tab") {
|
||||
#[allow(clippy::if_same_then_else)]
|
||||
let prevent_default = if egui_key == Some(Key::Tab) {
|
||||
// Always prevent moving cursor to url bar.
|
||||
// egui wants to use tab to move to the next text field.
|
||||
true
|
||||
} else if egui_key == Some(Key::P) {
|
||||
true // Prevent ctrl-P opening the print dialog. Users may want to use it for a command palette.
|
||||
} else if egui_wants_keyboard {
|
||||
matches!(
|
||||
event.key().as_str(),
|
||||
@@ -111,6 +119,7 @@ pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Res
|
||||
|
||||
if prevent_default {
|
||||
event.prevent_default();
|
||||
// event.stop_propagation();
|
||||
}
|
||||
},
|
||||
)?;
|
||||
@@ -125,6 +134,7 @@ pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Res
|
||||
runner_lock.input.raw.events.push(egui::Event::Key {
|
||||
key,
|
||||
pressed: false,
|
||||
repeat: false,
|
||||
modifiers,
|
||||
});
|
||||
}
|
||||
@@ -196,15 +206,21 @@ pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Res
|
||||
pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> {
|
||||
let canvas = canvas_element(runner_container.runner.lock().canvas_id()).unwrap();
|
||||
|
||||
{
|
||||
let prevent_default_events = [
|
||||
// By default, right-clicks open a context menu.
|
||||
// We don't want to do that (right clicks is handled by egui):
|
||||
let event_name = "contextmenu";
|
||||
"contextmenu",
|
||||
// Allow users to use ctrl-p for e.g. a command palette
|
||||
"afterprint",
|
||||
];
|
||||
|
||||
for event_name in prevent_default_events {
|
||||
let closure =
|
||||
move |event: web_sys::MouseEvent,
|
||||
mut _runner_lock: egui::mutex::MutexGuard<'_, AppRunner>| {
|
||||
event.prevent_default();
|
||||
// event.stop_propagation();
|
||||
// tracing::debug!("Preventing event {:?}", event_name);
|
||||
};
|
||||
|
||||
runner_container.add_event_listener(&canvas, event_name, closure)?;
|
||||
@@ -308,7 +324,7 @@ pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Resul
|
||||
modifiers,
|
||||
});
|
||||
|
||||
push_touches(&mut *runner_lock, egui::TouchPhase::Start, &event);
|
||||
push_touches(&mut runner_lock, egui::TouchPhase::Start, &event);
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
@@ -330,7 +346,7 @@ pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Resul
|
||||
.events
|
||||
.push(egui::Event::PointerMoved(pos));
|
||||
|
||||
push_touches(&mut *runner_lock, egui::TouchPhase::Move, &event);
|
||||
push_touches(&mut runner_lock, egui::TouchPhase::Move, &event);
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
@@ -357,7 +373,7 @@ pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Resul
|
||||
// Then remove hover effect:
|
||||
runner_lock.input.raw.events.push(egui::Event::PointerGone);
|
||||
|
||||
push_touches(&mut *runner_lock, egui::TouchPhase::End, &event);
|
||||
push_touches(&mut runner_lock, egui::TouchPhase::End, &event);
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
@@ -388,7 +404,7 @@ pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Resul
|
||||
}
|
||||
web_sys::WheelEvent::DOM_DELTA_LINE => {
|
||||
#[allow(clippy::let_and_return)]
|
||||
let points_per_scroll_line = 8.0; // Note that this is intentionally different from what we use in egui_glium / winit.
|
||||
let points_per_scroll_line = 8.0; // Note that this is intentionally different from what we use in winit.
|
||||
points_per_scroll_line
|
||||
}
|
||||
_ => 1.0, // DOM_DELTA_PIXEL
|
||||
|
||||
@@ -91,7 +91,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 {
|
||||
|
||||
@@ -15,7 +15,7 @@ pub fn load_memory(ctx: &egui::Context) {
|
||||
if let Some(memory_string) = local_storage_get("egui_memory_ron") {
|
||||
match ron::from_str(&memory_string) {
|
||||
Ok(memory) => {
|
||||
*ctx.memory() = memory;
|
||||
ctx.memory_mut(|m| *m = memory);
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("Failed to parse memory RON: {}", err);
|
||||
@@ -29,7 +29,7 @@ pub fn load_memory(_: &egui::Context) {}
|
||||
|
||||
#[cfg(feature = "persistence")]
|
||||
pub fn save_memory(ctx: &egui::Context) {
|
||||
match ron::to_string(&*ctx.memory()) {
|
||||
match ctx.memory(|mem| ron::to_string(mem)) {
|
||||
Ok(ron) => {
|
||||
local_storage_set("egui_memory_ron", &ron);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use egui::Rgba;
|
||||
use wasm_bindgen::JsValue;
|
||||
|
||||
/// Renderer for a browser canvas.
|
||||
@@ -19,7 +18,7 @@ pub(crate) trait WebPainter {
|
||||
/// Update all internal textures and paint gui.
|
||||
fn paint_and_update_textures(
|
||||
&mut self,
|
||||
clear_color: Rgba,
|
||||
clear_color: [f32; 4],
|
||||
clipped_primitives: &[egui::ClippedPrimitive],
|
||||
pixels_per_point: f32,
|
||||
textures_delta: &egui::TexturesDelta,
|
||||
|
||||
@@ -2,7 +2,6 @@ use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen::JsValue;
|
||||
use web_sys::HtmlCanvasElement;
|
||||
|
||||
use egui::Rgba;
|
||||
use egui_glow::glow;
|
||||
|
||||
use crate::{WebGlContextOption, WebOptions};
|
||||
@@ -49,7 +48,7 @@ impl WebPainter for WebPainterGlow {
|
||||
|
||||
fn paint_and_update_textures(
|
||||
&mut self,
|
||||
clear_color: Rgba,
|
||||
clear_color: [f32; 4],
|
||||
clipped_primitives: &[egui::ClippedPrimitive],
|
||||
pixels_per_point: f32,
|
||||
textures_delta: &egui::TexturesDelta,
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||
use wasm_bindgen::JsValue;
|
||||
use web_sys::HtmlCanvasElement;
|
||||
|
||||
use egui::{mutex::RwLock, Rgba};
|
||||
use egui::mutex::RwLock;
|
||||
use egui_wgpu::{renderer::ScreenDescriptor, RenderState, SurfaceErrorAction};
|
||||
|
||||
use crate::WebOptions;
|
||||
@@ -18,6 +18,8 @@ pub(crate) struct WebPainterWgpu {
|
||||
limits: wgpu::Limits,
|
||||
render_state: Option<RenderState>,
|
||||
on_surface_error: Arc<dyn Fn(wgpu::SurfaceError) -> SurfaceErrorAction>,
|
||||
depth_format: Option<wgpu::TextureFormat>,
|
||||
depth_texture_view: Option<wgpu::TextureView>,
|
||||
}
|
||||
|
||||
impl WebPainterWgpu {
|
||||
@@ -26,14 +28,46 @@ impl WebPainterWgpu {
|
||||
self.render_state.clone()
|
||||
}
|
||||
|
||||
pub fn generate_depth_texture_view(
|
||||
&self,
|
||||
render_state: &RenderState,
|
||||
width_in_pixels: u32,
|
||||
height_in_pixels: u32,
|
||||
) -> Option<wgpu::TextureView> {
|
||||
let device = &render_state.device;
|
||||
self.depth_format.map(|depth_format| {
|
||||
device
|
||||
.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("egui_depth_texture"),
|
||||
size: wgpu::Extent3d {
|
||||
width: width_in_pixels,
|
||||
height: height_in_pixels,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: depth_format,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
view_formats: &[depth_format],
|
||||
})
|
||||
.create_view(&wgpu::TextureViewDescriptor::default())
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(unused)] // only used if `wgpu` is the only active feature.
|
||||
pub async fn new(canvas_id: &str, options: &WebOptions) -> Result<Self, String> {
|
||||
tracing::debug!("Creating wgpu painter");
|
||||
|
||||
let canvas = super::canvas_element_or_die(canvas_id);
|
||||
|
||||
let instance = wgpu::Instance::new(options.wgpu_options.backends);
|
||||
let surface = instance.create_surface_from_canvas(&canvas);
|
||||
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
|
||||
backends: options.wgpu_options.backends,
|
||||
dx12_shader_compiler: Default::default(),
|
||||
});
|
||||
let surface = instance
|
||||
.create_surface_from_canvas(&canvas)
|
||||
.map_err(|err| format!("failed to create wgpu surface: {err}"))?;
|
||||
|
||||
let adapter = instance
|
||||
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||
@@ -53,9 +87,10 @@ impl WebPainterWgpu {
|
||||
.map_err(|err| format!("Failed to find wgpu device: {}", err))?;
|
||||
|
||||
let target_format =
|
||||
egui_wgpu::preferred_framebuffer_format(&surface.get_supported_formats(&adapter));
|
||||
egui_wgpu::preferred_framebuffer_format(&surface.get_capabilities(&adapter).formats);
|
||||
|
||||
let renderer = egui_wgpu::Renderer::new(&device, target_format, None, 1);
|
||||
let depth_format = options.wgpu_options.depth_format;
|
||||
let renderer = egui_wgpu::Renderer::new(&device, target_format, depth_format, 1);
|
||||
let render_state = RenderState {
|
||||
device: Arc::new(device),
|
||||
queue: Arc::new(queue),
|
||||
@@ -70,6 +105,7 @@ impl WebPainterWgpu {
|
||||
height: 0,
|
||||
present_mode: options.wgpu_options.present_mode,
|
||||
alpha_mode: wgpu::CompositeAlphaMode::Auto,
|
||||
view_formats: vec![target_format],
|
||||
};
|
||||
|
||||
tracing::debug!("wgpu painter initialized.");
|
||||
@@ -80,6 +116,8 @@ impl WebPainterWgpu {
|
||||
render_state: Some(render_state),
|
||||
surface,
|
||||
surface_configuration,
|
||||
depth_format,
|
||||
depth_texture_view: None,
|
||||
limits: options.wgpu_options.device_descriptor.limits.clone(),
|
||||
on_surface_error: options.wgpu_options.on_surface_error.clone(),
|
||||
})
|
||||
@@ -97,11 +135,13 @@ impl WebPainter for WebPainterWgpu {
|
||||
|
||||
fn paint_and_update_textures(
|
||||
&mut self,
|
||||
clear_color: Rgba,
|
||||
clear_color: [f32; 4],
|
||||
clipped_primitives: &[egui::ClippedPrimitive],
|
||||
pixels_per_point: f32,
|
||||
textures_delta: &egui::TexturesDelta,
|
||||
) -> Result<(), JsValue> {
|
||||
let size_in_pixels = [self.canvas.width(), self.canvas.height()];
|
||||
|
||||
let render_state = if let Some(render_state) = &self.render_state {
|
||||
render_state
|
||||
} else {
|
||||
@@ -110,32 +150,6 @@ impl WebPainter for WebPainterWgpu {
|
||||
));
|
||||
};
|
||||
|
||||
// Resize surface if needed
|
||||
let size_in_pixels = [self.canvas.width(), self.canvas.height()];
|
||||
if size_in_pixels[0] != self.surface_configuration.width
|
||||
|| size_in_pixels[1] != self.surface_configuration.height
|
||||
{
|
||||
self.surface_configuration.width = size_in_pixels[0];
|
||||
self.surface_configuration.height = size_in_pixels[1];
|
||||
self.surface
|
||||
.configure(&render_state.device, &self.surface_configuration);
|
||||
}
|
||||
|
||||
let frame = match self.surface.get_current_texture() {
|
||||
Ok(frame) => frame,
|
||||
#[allow(clippy::single_match_else)]
|
||||
Err(e) => match (*self.on_surface_error)(e) {
|
||||
SurfaceErrorAction::RecreateSurface => {
|
||||
self.surface
|
||||
.configure(&render_state.device, &self.surface_configuration);
|
||||
return Ok(());
|
||||
}
|
||||
SurfaceErrorAction::SkipFrame => {
|
||||
return Ok(());
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let mut encoder =
|
||||
render_state
|
||||
.device
|
||||
@@ -169,31 +183,77 @@ impl WebPainter for WebPainterWgpu {
|
||||
)
|
||||
};
|
||||
|
||||
{
|
||||
let renderer = render_state.renderer.read();
|
||||
let frame_view = frame
|
||||
.texture
|
||||
.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &frame_view,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color {
|
||||
r: clear_color.r() as f64,
|
||||
g: clear_color.g() as f64,
|
||||
b: clear_color.b() as f64,
|
||||
a: clear_color.a() as f64,
|
||||
}),
|
||||
store: true,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
label: Some("egui_render"),
|
||||
});
|
||||
// Resize surface if needed
|
||||
let is_zero_sized_surface = size_in_pixels[0] == 0 || size_in_pixels[1] == 0;
|
||||
let frame = if is_zero_sized_surface {
|
||||
None
|
||||
} else {
|
||||
if size_in_pixels[0] != self.surface_configuration.width
|
||||
|| size_in_pixels[1] != self.surface_configuration.height
|
||||
{
|
||||
self.surface_configuration.width = size_in_pixels[0];
|
||||
self.surface_configuration.height = size_in_pixels[1];
|
||||
self.surface
|
||||
.configure(&render_state.device, &self.surface_configuration);
|
||||
self.depth_texture_view = self.generate_depth_texture_view(
|
||||
render_state,
|
||||
size_in_pixels[0],
|
||||
size_in_pixels[1],
|
||||
);
|
||||
}
|
||||
|
||||
renderer.render(&mut render_pass, clipped_primitives, &screen_descriptor);
|
||||
}
|
||||
let frame = match self.surface.get_current_texture() {
|
||||
Ok(frame) => frame,
|
||||
#[allow(clippy::single_match_else)]
|
||||
Err(e) => match (*self.on_surface_error)(e) {
|
||||
SurfaceErrorAction::RecreateSurface => {
|
||||
self.surface
|
||||
.configure(&render_state.device, &self.surface_configuration);
|
||||
return Ok(());
|
||||
}
|
||||
SurfaceErrorAction::SkipFrame => {
|
||||
return Ok(());
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
{
|
||||
let renderer = render_state.renderer.read();
|
||||
let frame_view = frame
|
||||
.texture
|
||||
.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &frame_view,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color {
|
||||
r: clear_color[0] as f64,
|
||||
g: clear_color[1] as f64,
|
||||
b: clear_color[2] as f64,
|
||||
a: clear_color[3] as f64,
|
||||
}),
|
||||
store: true,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: self.depth_texture_view.as_ref().map(|view| {
|
||||
wgpu::RenderPassDepthStencilAttachment {
|
||||
view,
|
||||
depth_ops: Some(wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(1.0),
|
||||
store: false,
|
||||
}),
|
||||
stencil_ops: None,
|
||||
}
|
||||
}),
|
||||
label: Some("egui_render"),
|
||||
});
|
||||
|
||||
renderer.render(&mut render_pass, clipped_primitives, &screen_descriptor);
|
||||
}
|
||||
|
||||
Some(frame)
|
||||
};
|
||||
|
||||
{
|
||||
let mut renderer = render_state.renderer.write();
|
||||
@@ -208,7 +268,10 @@ impl WebPainter for WebPainterWgpu {
|
||||
.into_iter()
|
||||
.chain(std::iter::once(encoder.finish())),
|
||||
);
|
||||
frame.present();
|
||||
|
||||
if let Some(frame) = frame {
|
||||
frame.present();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3,16 +3,26 @@ All notable changes to the `egui-wgpu` integration will be noted in this file.
|
||||
|
||||
|
||||
## Unreleased
|
||||
* update to wgpu 0.15 ([#2629](https://github.com/emilk/egui/pull/2629))
|
||||
* Return `Err` instead of panic if we can't find a device ([#2428](https://github.com/emilk/egui/pull/2428)).
|
||||
* `winit::Painter::set_window` is now `async` ([#2434](https://github.com/emilk/egui/pull/2434)).
|
||||
* `egui-wgpu` now only depends on `epaint` instead of the entire `egui` ([#2438](https://github.com/emilk/egui/pull/2438)).
|
||||
|
||||
|
||||
## 0.20.0 - 2022-12-08 - web support
|
||||
* Renamed `RenderPass` to `Renderer`.
|
||||
* Renamed `RenderPass::execute` to `RenderPass::render`.
|
||||
* Renamed `RenderPass::execute_with_renderpass` to `Renderer::render` (replacing existing `Renderer::render`)
|
||||
* Reexported `Renderer`.
|
||||
* You can now use `egui-wgpu` on web, using WebGL ([#2107](https://github.com/emilk/egui/pull/2107)).
|
||||
* `Renderer` no longer handles pass creation and depth buffer creation ([#2136](https://github.com/emilk/egui/pull/2136))
|
||||
* `PrepareCallback` now passes `wgpu::CommandEncoder` ([#2136](https://github.com/emilk/egui/pull/2136))
|
||||
* `PrepareCallback` can now returns `wgpu::CommandBuffer` that are bundled into a single `wgpu::Queue::submit` call ([#2230](https://github.com/emilk/egui/pull/2230))
|
||||
* Only a single vertex & index buffer is now created and resized when necessary (previously, vertex/index buffers were allocated for every mesh) ([#2148](https://github.com/emilk/egui/pull/2148)).
|
||||
* `Renderer::update_texture` no longer creates a new `wgpu::Sampler` with every new texture ([#2198](https://github.com/emilk/egui/pull/2198))
|
||||
* `Painter`'s instance/device/adapter/surface creation is now configurable via `WgpuConfiguration` ([#2207](https://github.com/emilk/egui/pull/2207))
|
||||
* Fix panic on using a depth buffer ([#2316](https://github.com/emilk/egui/pull/2316))
|
||||
|
||||
|
||||
## 0.19.0 - 2022-08-20
|
||||
* Enables deferred render + surface state initialization for Android ([#1634](https://github.com/emilk/egui/pull/1634)).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "egui-wgpu"
|
||||
version = "0.19.0"
|
||||
version = "0.20.0"
|
||||
description = "Bindings for using egui natively using the wgpu library"
|
||||
authors = [
|
||||
"Nils Hasenbanck <nils@hasenbanck.de>",
|
||||
@@ -8,7 +8,7 @@ authors = [
|
||||
"Emil Ernerfeldt <emil.ernerfeldt@gmail.com>",
|
||||
]
|
||||
edition = "2021"
|
||||
rust-version = "1.62"
|
||||
rust-version = "1.65"
|
||||
homepage = "https://github.com/emilk/egui/tree/master/crates/egui-wgpu"
|
||||
license = "MIT OR Apache-2.0"
|
||||
readme = "README.md"
|
||||
@@ -32,25 +32,24 @@ all-features = true
|
||||
puffin = ["dep:puffin"]
|
||||
|
||||
## Enable [`winit`](https://docs.rs/winit) integration.
|
||||
winit = ["dep:pollster", "dep:winit"]
|
||||
winit = ["dep:winit"]
|
||||
|
||||
|
||||
[dependencies]
|
||||
egui = { version = "0.19.0", path = "../egui", default-features = false, features = [
|
||||
epaint = { version = "0.20.0", path = "../epaint", default-features = false, features = [
|
||||
"bytemuck",
|
||||
] }
|
||||
|
||||
bytemuck = "1.7"
|
||||
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
||||
type-map = "0.5.0"
|
||||
wgpu = "0.14"
|
||||
wgpu = "0.15.0"
|
||||
|
||||
#! ### Optional dependencies
|
||||
## Enable this when generating docs.
|
||||
document-features = { version = "0.2", optional = true }
|
||||
|
||||
pollster = { version = "0.2", optional = true }
|
||||
winit = { version = "0.27.2", optional = true }
|
||||
winit = { version = "0.28", optional = true }
|
||||
|
||||
# Native:
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
|
||||
@@ -8,18 +8,19 @@
|
||||
|
||||
pub use wgpu;
|
||||
|
||||
/// Low-level painting of [`egui`] on [`wgpu`].
|
||||
/// Low-level painting of [`egui`](https://github.com/emilk/egui) on [`wgpu`].
|
||||
pub mod renderer;
|
||||
pub use renderer::CallbackFn;
|
||||
pub use renderer::Renderer;
|
||||
|
||||
/// Module for painting [`egui`] with [`wgpu`] on [`winit`].
|
||||
/// Module for painting [`egui`](https://github.com/emilk/egui) with [`wgpu`] on [`winit`].
|
||||
#[cfg(feature = "winit")]
|
||||
pub mod winit;
|
||||
|
||||
use egui::mutex::RwLock;
|
||||
use std::sync::Arc;
|
||||
|
||||
use epaint::mutex::RwLock;
|
||||
|
||||
/// Access to the render state for egui.
|
||||
#[derive(Clone)]
|
||||
pub struct RenderState {
|
||||
@@ -55,6 +56,8 @@ pub struct WgpuConfiguration {
|
||||
|
||||
/// Callback for surface errors.
|
||||
pub on_surface_error: Arc<dyn Fn(wgpu::SurfaceError) -> SurfaceErrorAction>,
|
||||
|
||||
pub depth_format: Option<wgpu::TextureFormat>,
|
||||
}
|
||||
|
||||
impl Default for WgpuConfiguration {
|
||||
@@ -68,6 +71,7 @@ impl Default for WgpuConfiguration {
|
||||
backends: wgpu::Backends::PRIMARY | wgpu::Backends::GL,
|
||||
present_mode: wgpu::PresentMode::AutoVsync,
|
||||
power_preference: wgpu::PowerPreference::HighPerformance,
|
||||
depth_format: None,
|
||||
|
||||
on_surface_error: Arc::new(|err| {
|
||||
if err == wgpu::SurfaceError::Outdated {
|
||||
@@ -95,7 +99,35 @@ pub fn preferred_framebuffer_format(formats: &[wgpu::TextureFormat]) -> wgpu::Te
|
||||
}
|
||||
formats[0] // take the first
|
||||
}
|
||||
|
||||
// maybe use this-error?
|
||||
#[derive(Debug)]
|
||||
pub enum WgpuError {
|
||||
DeviceError(wgpu::RequestDeviceError),
|
||||
SurfaceError(wgpu::CreateSurfaceError),
|
||||
}
|
||||
impl std::fmt::Display for WgpuError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
std::fmt::Debug::fmt(self, f)
|
||||
}
|
||||
}
|
||||
impl std::error::Error for WgpuError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
WgpuError::DeviceError(e) => e.source(),
|
||||
WgpuError::SurfaceError(e) => e.source(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<wgpu::RequestDeviceError> for WgpuError {
|
||||
fn from(e: wgpu::RequestDeviceError) -> Self {
|
||||
Self::DeviceError(e)
|
||||
}
|
||||
}
|
||||
impl From<wgpu::CreateSurfaceError> for WgpuError {
|
||||
fn from(e: wgpu::CreateSurfaceError) -> Self {
|
||||
Self::SurfaceError(e)
|
||||
}
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Profiling macro for feature "puffin"
|
||||
|
||||
@@ -4,14 +4,13 @@ use std::num::NonZeroU64;
|
||||
use std::ops::Range;
|
||||
use std::{borrow::Cow, collections::HashMap, num::NonZeroU32};
|
||||
|
||||
use egui::epaint::Vertex;
|
||||
use egui::NumExt;
|
||||
use egui::{epaint::Primitive, PaintCallbackInfo};
|
||||
use type_map::concurrent::TypeMap;
|
||||
use wgpu;
|
||||
use wgpu::util::DeviceExt as _;
|
||||
|
||||
/// A callback function that can be used to compose an [`egui::PaintCallback`] for custom WGPU
|
||||
use epaint::{emath::NumExt, PaintCallbackInfo, Primitive, Vertex};
|
||||
|
||||
/// A callback function that can be used to compose an [`epaint::PaintCallback`] for custom WGPU
|
||||
/// rendering.
|
||||
///
|
||||
/// The callback is composed of two functions: `prepare` and `paint`:
|
||||
@@ -154,11 +153,11 @@ pub struct Renderer {
|
||||
/// Map of egui texture IDs to textures and their associated bindgroups (texture view +
|
||||
/// sampler). The texture may be None if the TextureId is just a handle to a user-provided
|
||||
/// sampler.
|
||||
textures: HashMap<egui::TextureId, (Option<wgpu::Texture>, wgpu::BindGroup)>,
|
||||
textures: HashMap<epaint::TextureId, (Option<wgpu::Texture>, wgpu::BindGroup)>,
|
||||
next_user_texture_id: u64,
|
||||
samplers: HashMap<egui::TextureOptions, wgpu::Sampler>,
|
||||
samplers: HashMap<epaint::textures::TextureOptions, wgpu::Sampler>,
|
||||
|
||||
/// Storage for use by [`egui::PaintCallback`]'s that need to store resources such as render
|
||||
/// Storage for use by [`epaint::PaintCallback`]'s that need to store resources such as render
|
||||
/// pipelines that must have the lifetime of the renderpass.
|
||||
pub paint_callback_resources: TypeMap,
|
||||
}
|
||||
@@ -346,7 +345,7 @@ impl Renderer {
|
||||
pub fn render<'rp>(
|
||||
&'rp self,
|
||||
render_pass: &mut wgpu::RenderPass<'rp>,
|
||||
paint_jobs: &[egui::epaint::ClippedPrimitive],
|
||||
paint_jobs: &[epaint::ClippedPrimitive],
|
||||
screen_descriptor: &ScreenDescriptor,
|
||||
) {
|
||||
crate::profile_function!();
|
||||
@@ -361,7 +360,7 @@ impl Renderer {
|
||||
let mut index_buffer_slices = self.index_buffer.slices.iter();
|
||||
let mut vertex_buffer_slices = self.vertex_buffer.slices.iter();
|
||||
|
||||
for egui::ClippedPrimitive {
|
||||
for epaint::ClippedPrimitive {
|
||||
clip_rect,
|
||||
primitive,
|
||||
} in paint_jobs
|
||||
@@ -475,8 +474,8 @@ impl Renderer {
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
id: egui::TextureId,
|
||||
image_delta: &egui::epaint::ImageDelta,
|
||||
id: epaint::TextureId,
|
||||
image_delta: &epaint::ImageDelta,
|
||||
) {
|
||||
crate::profile_function!();
|
||||
|
||||
@@ -490,7 +489,7 @@ impl Renderer {
|
||||
};
|
||||
|
||||
let data_color32 = match &image_delta.image {
|
||||
egui::ImageData::Color(image) => {
|
||||
epaint::ImageData::Color(image) => {
|
||||
assert_eq!(
|
||||
width as usize * height as usize,
|
||||
image.pixels.len(),
|
||||
@@ -498,7 +497,7 @@ impl Renderer {
|
||||
);
|
||||
Cow::Borrowed(&image.pixels)
|
||||
}
|
||||
egui::ImageData::Font(image) => {
|
||||
epaint::ImageData::Font(image) => {
|
||||
assert_eq!(
|
||||
width as usize * height as usize,
|
||||
image.pixels.len(),
|
||||
@@ -555,6 +554,7 @@ impl Renderer {
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Rgba8UnormSrgb, // Minspec for wgpu WebGL emulation is WebGL2, so this should always be supported.
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
||||
view_formats: &[wgpu::TextureFormat::Rgba8UnormSrgb],
|
||||
});
|
||||
let sampler = self
|
||||
.samplers
|
||||
@@ -582,7 +582,7 @@ impl Renderer {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn free_texture(&mut self, id: &egui::TextureId) {
|
||||
pub fn free_texture(&mut self, id: &epaint::TextureId) {
|
||||
self.textures.remove(id);
|
||||
}
|
||||
|
||||
@@ -590,15 +590,15 @@ impl Renderer {
|
||||
///
|
||||
/// This could be used by custom paint hooks to render images that have been added through with
|
||||
/// [`egui_extras::RetainedImage`](https://docs.rs/egui_extras/latest/egui_extras/image/struct.RetainedImage.html)
|
||||
/// or [`egui::Context::load_texture`].
|
||||
/// or [`epaint::Context::load_texture`](https://docs.rs/egui/latest/egui/struct.Context.html#method.load_texture).
|
||||
pub fn texture(
|
||||
&self,
|
||||
id: &egui::TextureId,
|
||||
id: &epaint::TextureId,
|
||||
) -> Option<&(Option<wgpu::Texture>, wgpu::BindGroup)> {
|
||||
self.textures.get(id)
|
||||
}
|
||||
|
||||
/// Registers a `wgpu::Texture` with a `egui::TextureId`.
|
||||
/// Registers a `wgpu::Texture` with a `epaint::TextureId`.
|
||||
///
|
||||
/// This enables the application to reference the texture inside an image ui element.
|
||||
/// This effectively enables off-screen rendering inside the egui UI. Texture must have
|
||||
@@ -609,7 +609,7 @@ impl Renderer {
|
||||
device: &wgpu::Device,
|
||||
texture: &wgpu::TextureView,
|
||||
texture_filter: wgpu::FilterMode,
|
||||
) -> egui::TextureId {
|
||||
) -> epaint::TextureId {
|
||||
self.register_native_texture_with_sampler_options(
|
||||
device,
|
||||
texture,
|
||||
@@ -622,7 +622,7 @@ impl Renderer {
|
||||
)
|
||||
}
|
||||
|
||||
/// Registers a `wgpu::Texture` with an existing `egui::TextureId`.
|
||||
/// Registers a `wgpu::Texture` with an existing `epaint::TextureId`.
|
||||
///
|
||||
/// This enables applications to reuse `TextureId`s.
|
||||
pub fn update_egui_texture_from_wgpu_texture(
|
||||
@@ -630,7 +630,7 @@ impl Renderer {
|
||||
device: &wgpu::Device,
|
||||
texture: &wgpu::TextureView,
|
||||
texture_filter: wgpu::FilterMode,
|
||||
id: egui::TextureId,
|
||||
id: epaint::TextureId,
|
||||
) {
|
||||
self.update_egui_texture_from_wgpu_texture_with_sampler_options(
|
||||
device,
|
||||
@@ -645,7 +645,7 @@ impl Renderer {
|
||||
);
|
||||
}
|
||||
|
||||
/// Registers a `wgpu::Texture` with a `egui::TextureId` while also accepting custom
|
||||
/// Registers a `wgpu::Texture` with a `epaint::TextureId` while also accepting custom
|
||||
/// `wgpu::SamplerDescriptor` options.
|
||||
///
|
||||
/// This allows applications to specify individual minification/magnification filters as well as
|
||||
@@ -660,7 +660,7 @@ impl Renderer {
|
||||
device: &wgpu::Device,
|
||||
texture: &wgpu::TextureView,
|
||||
sampler_descriptor: wgpu::SamplerDescriptor<'_>,
|
||||
) -> egui::TextureId {
|
||||
) -> epaint::TextureId {
|
||||
crate::profile_function!();
|
||||
|
||||
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||
@@ -683,14 +683,14 @@ impl Renderer {
|
||||
],
|
||||
});
|
||||
|
||||
let id = egui::TextureId::User(self.next_user_texture_id);
|
||||
let id = epaint::TextureId::User(self.next_user_texture_id);
|
||||
self.textures.insert(id, (None, bind_group));
|
||||
self.next_user_texture_id += 1;
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
/// Registers a `wgpu::Texture` with an existing `egui::TextureId` while also accepting custom
|
||||
/// Registers a `wgpu::Texture` with an existing `epaint::TextureId` while also accepting custom
|
||||
/// `wgpu::SamplerDescriptor` options.
|
||||
///
|
||||
/// This allows applications to reuse `TextureId`s created with custom sampler options.
|
||||
@@ -700,7 +700,7 @@ impl Renderer {
|
||||
device: &wgpu::Device,
|
||||
texture: &wgpu::TextureView,
|
||||
sampler_descriptor: wgpu::SamplerDescriptor<'_>,
|
||||
id: egui::TextureId,
|
||||
id: epaint::TextureId,
|
||||
) {
|
||||
crate::profile_function!();
|
||||
|
||||
@@ -741,7 +741,7 @@ impl Renderer {
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
paint_jobs: &[egui::epaint::ClippedPrimitive],
|
||||
paint_jobs: &[epaint::ClippedPrimitive],
|
||||
screen_descriptor: &ScreenDescriptor,
|
||||
) -> Vec<wgpu::CommandBuffer> {
|
||||
crate::profile_function!();
|
||||
@@ -801,7 +801,7 @@ impl Renderer {
|
||||
let mut user_cmd_bufs = Vec::new(); // collect user command buffers
|
||||
|
||||
crate::profile_scope!("primitives");
|
||||
for egui::ClippedPrimitive { primitive, .. } in paint_jobs.iter() {
|
||||
for epaint::ClippedPrimitive { primitive, .. } in paint_jobs.iter() {
|
||||
match primitive {
|
||||
Primitive::Mesh(mesh) => {
|
||||
{
|
||||
@@ -844,14 +844,17 @@ impl Renderer {
|
||||
}
|
||||
}
|
||||
|
||||
fn create_sampler(options: egui::TextureOptions, device: &wgpu::Device) -> wgpu::Sampler {
|
||||
fn create_sampler(
|
||||
options: epaint::textures::TextureOptions,
|
||||
device: &wgpu::Device,
|
||||
) -> wgpu::Sampler {
|
||||
let mag_filter = match options.magnification {
|
||||
egui::TextureFilter::Nearest => wgpu::FilterMode::Nearest,
|
||||
egui::TextureFilter::Linear => wgpu::FilterMode::Linear,
|
||||
epaint::textures::TextureFilter::Nearest => wgpu::FilterMode::Nearest,
|
||||
epaint::textures::TextureFilter::Linear => wgpu::FilterMode::Linear,
|
||||
};
|
||||
let min_filter = match options.minification {
|
||||
egui::TextureFilter::Nearest => wgpu::FilterMode::Nearest,
|
||||
egui::TextureFilter::Linear => wgpu::FilterMode::Linear,
|
||||
epaint::textures::TextureFilter::Nearest => wgpu::FilterMode::Nearest,
|
||||
epaint::textures::TextureFilter::Linear => wgpu::FilterMode::Linear,
|
||||
};
|
||||
device.create_sampler(&wgpu::SamplerDescriptor {
|
||||
label: Some(&format!(
|
||||
@@ -893,7 +896,7 @@ struct ScissorRect {
|
||||
}
|
||||
|
||||
impl ScissorRect {
|
||||
fn new(clip_rect: &egui::Rect, pixels_per_point: f32, target_size: [u32; 2]) -> Self {
|
||||
fn new(clip_rect: &epaint::Rect, pixels_per_point: f32, target_size: [u32; 2]) -> Self {
|
||||
// Transform clip rect to physical pixels:
|
||||
let clip_min_x = pixels_per_point * clip_rect.min.x;
|
||||
let clip_min_y = pixels_per_point * clip_rect.min.y;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use egui::mutex::RwLock;
|
||||
use tracing::error;
|
||||
use wgpu::{Adapter, Instance, Surface};
|
||||
|
||||
use epaint::mutex::RwLock;
|
||||
|
||||
use crate::{renderer, RenderState, Renderer, SurfaceErrorAction, WgpuConfiguration};
|
||||
|
||||
struct SurfaceState {
|
||||
@@ -41,12 +42,15 @@ impl Painter {
|
||||
/// a [`winit::window::Window`] with a valid `.raw_window_handle()`
|
||||
/// associated.
|
||||
pub fn new(configuration: WgpuConfiguration, msaa_samples: u32, depth_bits: u8) -> Self {
|
||||
let instance = wgpu::Instance::new(configuration.backends);
|
||||
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
|
||||
backends: configuration.backends,
|
||||
dx12_shader_compiler: Default::default(), //
|
||||
});
|
||||
|
||||
Self {
|
||||
configuration,
|
||||
msaa_samples,
|
||||
depth_format: (depth_bits > 0).then(|| wgpu::TextureFormat::Depth32Float),
|
||||
depth_format: (depth_bits > 0).then_some(wgpu::TextureFormat::Depth32Float),
|
||||
depth_texture_view: None,
|
||||
|
||||
instance,
|
||||
@@ -60,26 +64,27 @@ impl Painter {
|
||||
///
|
||||
/// Will return [`None`] if the render state has not been initialized yet.
|
||||
pub fn render_state(&self) -> Option<RenderState> {
|
||||
self.render_state.as_ref().cloned()
|
||||
self.render_state.clone()
|
||||
}
|
||||
|
||||
async fn init_render_state(
|
||||
&self,
|
||||
adapter: &Adapter,
|
||||
target_format: wgpu::TextureFormat,
|
||||
) -> RenderState {
|
||||
let (device, queue) =
|
||||
pollster::block_on(adapter.request_device(&self.configuration.device_descriptor, None))
|
||||
.unwrap();
|
||||
|
||||
let renderer = Renderer::new(&device, target_format, self.depth_format, self.msaa_samples);
|
||||
|
||||
RenderState {
|
||||
device: Arc::new(device),
|
||||
queue: Arc::new(queue),
|
||||
target_format,
|
||||
renderer: Arc::new(RwLock::new(renderer)),
|
||||
}
|
||||
) -> Result<RenderState, wgpu::RequestDeviceError> {
|
||||
adapter
|
||||
.request_device(&self.configuration.device_descriptor, None)
|
||||
.await
|
||||
.map(|(device, queue)| {
|
||||
let renderer =
|
||||
Renderer::new(&device, target_format, self.depth_format, self.msaa_samples);
|
||||
RenderState {
|
||||
device: Arc::new(device),
|
||||
queue: Arc::new(queue),
|
||||
target_format,
|
||||
renderer: Arc::new(RwLock::new(renderer)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// We want to defer the initialization of our render state until we have a surface
|
||||
@@ -87,25 +92,33 @@ impl Painter {
|
||||
//
|
||||
// After we've initialized our render state once though we expect all future surfaces
|
||||
// will have the same format and so this render state will remain valid.
|
||||
fn ensure_render_state_for_surface(&mut self, surface: &Surface) {
|
||||
self.adapter.get_or_insert_with(|| {
|
||||
pollster::block_on(self.instance.request_adapter(&wgpu::RequestAdapterOptions {
|
||||
power_preference: self.configuration.power_preference,
|
||||
compatible_surface: Some(surface),
|
||||
force_fallback_adapter: false,
|
||||
}))
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
if self.render_state.is_none() {
|
||||
let adapter = self.adapter.as_ref().unwrap();
|
||||
|
||||
let swapchain_format =
|
||||
crate::preferred_framebuffer_format(&surface.get_supported_formats(adapter));
|
||||
|
||||
let rs = pollster::block_on(self.init_render_state(adapter, swapchain_format));
|
||||
self.render_state = Some(rs);
|
||||
async fn ensure_render_state_for_surface(
|
||||
&mut self,
|
||||
surface: &Surface,
|
||||
) -> Result<(), wgpu::RequestDeviceError> {
|
||||
if self.adapter.is_none() {
|
||||
self.adapter = self
|
||||
.instance
|
||||
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||
power_preference: self.configuration.power_preference,
|
||||
compatible_surface: Some(surface),
|
||||
force_fallback_adapter: false,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
if self.render_state.is_none() {
|
||||
match &self.adapter {
|
||||
Some(adapter) => {
|
||||
let swapchain_format = crate::preferred_framebuffer_format(
|
||||
&surface.get_capabilities(adapter).formats,
|
||||
);
|
||||
let rs = self.init_render_state(adapter, swapchain_format).await?;
|
||||
self.render_state = Some(rs);
|
||||
}
|
||||
None => return Err(wgpu::RequestDeviceError {}),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn configure_surface(&mut self, width_in_pixels: u32, height_in_pixels: u32) {
|
||||
@@ -124,6 +137,7 @@ impl Painter {
|
||||
height: height_in_pixels,
|
||||
present_mode: self.configuration.present_mode,
|
||||
alpha_mode: wgpu::CompositeAlphaMode::Auto,
|
||||
view_formats: vec![format],
|
||||
};
|
||||
|
||||
let surface_state = self
|
||||
@@ -161,12 +175,18 @@ impl Painter {
|
||||
/// The raw Window handle associated with the given `window` must be a valid object to create a
|
||||
/// surface upon and must remain valid for the lifetime of the created surface. (The surface may
|
||||
/// be cleared by passing `None`).
|
||||
pub unsafe fn set_window(&mut self, window: Option<&winit::window::Window>) {
|
||||
///
|
||||
/// # Errors
|
||||
/// If the provided wgpu configuration does not match an available device.
|
||||
pub async unsafe fn set_window(
|
||||
&mut self,
|
||||
window: Option<&winit::window::Window>,
|
||||
) -> Result<(), crate::WgpuError> {
|
||||
match window {
|
||||
Some(window) => {
|
||||
let surface = self.instance.create_surface(&window);
|
||||
let surface = self.instance.create_surface(&window)?;
|
||||
|
||||
self.ensure_render_state_for_surface(&surface);
|
||||
self.ensure_render_state_for_surface(&surface).await?;
|
||||
|
||||
let size = window.inner_size();
|
||||
let width = size.width;
|
||||
@@ -176,12 +196,13 @@ impl Painter {
|
||||
width,
|
||||
height,
|
||||
});
|
||||
self.configure_surface(width, height);
|
||||
self.resize_and_generate_depth_texture_view(width, height);
|
||||
}
|
||||
None => {
|
||||
self.surface_state = None;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the maximum texture dimension supported if known
|
||||
@@ -195,28 +216,37 @@ impl Painter {
|
||||
.map(|rs| rs.device.limits().max_texture_dimension_2d as usize)
|
||||
}
|
||||
|
||||
fn resize_and_generate_depth_texture_view(
|
||||
&mut self,
|
||||
width_in_pixels: u32,
|
||||
height_in_pixels: u32,
|
||||
) {
|
||||
self.configure_surface(width_in_pixels, height_in_pixels);
|
||||
let device = &self.render_state.as_ref().unwrap().device;
|
||||
self.depth_texture_view = self.depth_format.map(|depth_format| {
|
||||
device
|
||||
.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("egui_depth_texture"),
|
||||
size: wgpu::Extent3d {
|
||||
width: width_in_pixels,
|
||||
height: height_in_pixels,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: depth_format,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
|
||||
| wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[depth_format],
|
||||
})
|
||||
.create_view(&wgpu::TextureViewDescriptor::default())
|
||||
});
|
||||
}
|
||||
|
||||
pub fn on_window_resized(&mut self, width_in_pixels: u32, height_in_pixels: u32) {
|
||||
if self.surface_state.is_some() {
|
||||
self.configure_surface(width_in_pixels, height_in_pixels);
|
||||
let device = &self.render_state.as_ref().unwrap().device;
|
||||
self.depth_texture_view = self.depth_format.map(|depth_format| {
|
||||
device
|
||||
.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("egui_depth_texture"),
|
||||
size: wgpu::Extent3d {
|
||||
width: width_in_pixels,
|
||||
height: height_in_pixels,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: depth_format,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
|
||||
| wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
})
|
||||
.create_view(&wgpu::TextureViewDescriptor::default())
|
||||
});
|
||||
self.resize_and_generate_depth_texture_view(width_in_pixels, height_in_pixels);
|
||||
} else {
|
||||
error!("Ignoring window resize notification with no surface created via Painter::set_window()");
|
||||
}
|
||||
@@ -225,9 +255,9 @@ impl Painter {
|
||||
pub fn paint_and_update_textures(
|
||||
&mut self,
|
||||
pixels_per_point: f32,
|
||||
clear_color: egui::Rgba,
|
||||
clipped_primitives: &[egui::ClippedPrimitive],
|
||||
textures_delta: &egui::TexturesDelta,
|
||||
clear_color: [f32; 4],
|
||||
clipped_primitives: &[epaint::ClippedPrimitive],
|
||||
textures_delta: &epaint::textures::TexturesDelta,
|
||||
) {
|
||||
crate::profile_function!();
|
||||
|
||||
@@ -305,10 +335,10 @@ impl Painter {
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color {
|
||||
r: clear_color.r() as f64,
|
||||
g: clear_color.g() as f64,
|
||||
b: clear_color.b() as f64,
|
||||
a: clear_color.a() as f64,
|
||||
r: clear_color[0] as f64,
|
||||
g: clear_color[1] as f64,
|
||||
b: clear_color[2] as f64,
|
||||
a: clear_color[3] as f64,
|
||||
}),
|
||||
store: true,
|
||||
},
|
||||
|
||||
@@ -3,8 +3,19 @@ All notable changes to the `egui-winit` integration will be noted in this file.
|
||||
|
||||
|
||||
## Unreleased
|
||||
* The default features of the `winit` crate are not enabled if the default features of `egui-winit` are disabled too ([#1971](https://github.com/emilk/egui/pull/1971))
|
||||
* Added new feature `wayland` which enables Wayland support ([#1971](https://github.com/emilk/egui/pull/1971))
|
||||
* Update to `winit` 0.28, adding support for mac trackpad zoom ([#2654](https://github.com/emilk/egui/pull/2654)).
|
||||
* Remove the `screen_reader` feature. Use the `accesskit` feature flag instead ([#2669](https://github.com/emilk/egui/pull/2669)).
|
||||
|
||||
|
||||
## 0.20.1 - 2022-12-11
|
||||
* Fix docs.rs build ([#2420](https://github.com/emilk/egui/pull/2420)).
|
||||
|
||||
|
||||
## 0.20.0 - 2022-12-08
|
||||
* The default features of the `winit` crate are not enabled if the default features of `egui-winit` are disabled too ([#1971](https://github.com/emilk/egui/pull/1971)).
|
||||
* Added new feature `wayland` which enables Wayland support ([#1971](https://github.com/emilk/egui/pull/1971)).
|
||||
* Don't repaint when just moving window ([#1980](https://github.com/emilk/egui/pull/1980)).
|
||||
* Added optional integration with [AccessKit](https://accesskit.dev/) for implementing platform accessibility APIs ([#2294](https://github.com/emilk/egui/pull/2294)).
|
||||
|
||||
## 0.19.0 - 2022-08-20
|
||||
* MSRV (Minimum Supported Rust Version) is now `1.61.0` ([#1846](https://github.com/emilk/egui/pull/1846)).
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
[package]
|
||||
name = "egui-winit"
|
||||
version = "0.19.0"
|
||||
version = "0.20.1"
|
||||
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
|
||||
description = "Bindings for using egui with winit"
|
||||
edition = "2021"
|
||||
rust-version = "1.62"
|
||||
rust-version = "1.65"
|
||||
homepage = "https://github.com/emilk/egui/tree/master/crates/egui-winit"
|
||||
license = "MIT OR Apache-2.0"
|
||||
readme = "README.md"
|
||||
@@ -20,6 +20,9 @@ all-features = true
|
||||
[features]
|
||||
default = ["clipboard", "links", "wayland", "winit/default"]
|
||||
|
||||
## Enable platform accessibility API implementations through [AccessKit](https://accesskit.dev/).
|
||||
accesskit = ["accesskit_winit", "egui/accesskit"]
|
||||
|
||||
## [`bytemuck`](https://docs.rs/bytemuck) enables you to cast [`egui::epaint::Vertex`], [`egui::Vec2`] etc to `&[u8]`.
|
||||
bytemuck = ["egui/bytemuck"]
|
||||
|
||||
@@ -33,9 +36,6 @@ links = ["webbrowser"]
|
||||
## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate.
|
||||
puffin = ["dep:puffin"]
|
||||
|
||||
## Experimental support for a screen reader.
|
||||
screen_reader = ["tts"]
|
||||
|
||||
## Allow serialization of [`WindowSettings`] using [`serde`](https://docs.rs/serde).
|
||||
serde = ["egui/serde", "dep:serde"]
|
||||
|
||||
@@ -43,30 +43,34 @@ serde = ["egui/serde", "dep:serde"]
|
||||
wayland = ["winit/wayland"]
|
||||
|
||||
[dependencies]
|
||||
egui = { version = "0.19.0", path = "../egui", default-features = false, features = [
|
||||
egui = { version = "0.20.0", path = "../egui", default-features = false, features = [
|
||||
"tracing",
|
||||
] }
|
||||
instant = { version = "0.1", features = [
|
||||
"wasm-bindgen",
|
||||
] } # We use instant so we can (maybe) compile for web
|
||||
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
||||
winit = { version = "0.27.2", default-features = false }
|
||||
winit = { version = "0.28", default-features = false }
|
||||
|
||||
#! ### Optional dependencies
|
||||
|
||||
# feature accesskit
|
||||
accesskit_winit = { version = "0.9.0", optional = true }
|
||||
|
||||
## Enable this when generating docs.
|
||||
document-features = { version = "0.2", optional = true }
|
||||
|
||||
puffin = { version = "0.14", optional = true }
|
||||
serde = { version = "1.0", optional = true, features = ["derive"] }
|
||||
|
||||
# feature screen_reader
|
||||
tts = { version = "0.24", optional = true }
|
||||
|
||||
webbrowser = { version = "0.8", optional = true }
|
||||
webbrowser = { version = "0.8.3", optional = true }
|
||||
|
||||
[target.'cfg(any(target_os="linux", target_os="dragonfly", target_os="freebsd", target_os="netbsd", target_os="openbsd"))'.dependencies]
|
||||
smithay-clipboard = { version = "0.6.3", optional = true }
|
||||
|
||||
[target.'cfg(not(target_os = "android"))'.dependencies]
|
||||
arboard = { version = "3.2", optional = true, default-features = false }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
# TODO(emilk): this is probably not the right place for specifying native-activity, but we need to do it somewhere for the CI
|
||||
android-activity = { version = "0.4", features = ["native-activity"] }
|
||||
|
||||
@@ -30,6 +30,7 @@ impl Clipboard {
|
||||
Self {
|
||||
#[cfg(all(feature = "arboard", not(target_os = "android")))]
|
||||
arboard: init_arboard(),
|
||||
|
||||
#[cfg(all(
|
||||
any(
|
||||
target_os = "linux",
|
||||
@@ -41,6 +42,7 @@ impl Clipboard {
|
||||
feature = "smithay-clipboard"
|
||||
))]
|
||||
smithay: init_smithay_clipboard(wayland_display),
|
||||
|
||||
clipboard: Default::default(),
|
||||
}
|
||||
}
|
||||
@@ -60,7 +62,7 @@ impl Clipboard {
|
||||
return match clipboard.load() {
|
||||
Ok(text) => Some(text),
|
||||
Err(err) => {
|
||||
tracing::error!("Paste error: {}", err);
|
||||
tracing::error!("smithay paste error: {err}");
|
||||
None
|
||||
}
|
||||
};
|
||||
@@ -71,7 +73,7 @@ impl Clipboard {
|
||||
return match clipboard.get_text() {
|
||||
Ok(text) => Some(text),
|
||||
Err(err) => {
|
||||
tracing::error!("Paste error: {}", err);
|
||||
tracing::error!("arboard paste error: {err}");
|
||||
None
|
||||
}
|
||||
};
|
||||
@@ -99,7 +101,7 @@ impl Clipboard {
|
||||
#[cfg(all(feature = "arboard", not(target_os = "android")))]
|
||||
if let Some(clipboard) = &mut self.arboard {
|
||||
if let Err(err) = clipboard.set_text(text) {
|
||||
tracing::error!("Copy/Cut error: {}", err);
|
||||
tracing::error!("arboard copy/cut error: {err}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -110,10 +112,11 @@ impl Clipboard {
|
||||
|
||||
#[cfg(all(feature = "arboard", not(target_os = "android")))]
|
||||
fn init_arboard() -> Option<arboard::Clipboard> {
|
||||
tracing::debug!("Initializing arboard clipboard…");
|
||||
match arboard::Clipboard::new() {
|
||||
Ok(clipboard) => Some(clipboard),
|
||||
Err(err) => {
|
||||
tracing::error!("Failed to initialize clipboard: {}", err);
|
||||
tracing::warn!("Failed to initialize arboard clipboard: {err}");
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -133,10 +136,11 @@ fn init_smithay_clipboard(
|
||||
wayland_display: Option<*mut c_void>,
|
||||
) -> Option<smithay_clipboard::Clipboard> {
|
||||
if let Some(display) = wayland_display {
|
||||
tracing::debug!("Initializing smithay clipboard…");
|
||||
#[allow(unsafe_code)]
|
||||
Some(unsafe { smithay_clipboard::Clipboard::new(display) })
|
||||
} else {
|
||||
tracing::error!("Cannot initialize smithay clipboard without a display handle!");
|
||||
tracing::debug!("Cannot initialize smithay clipboard without a display handle");
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,27 +11,20 @@
|
||||
|
||||
use std::os::raw::c_void;
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub use accesskit_winit;
|
||||
pub use egui;
|
||||
#[cfg(feature = "accesskit")]
|
||||
use egui::accesskit;
|
||||
pub use winit;
|
||||
|
||||
pub mod clipboard;
|
||||
pub mod screen_reader;
|
||||
mod window_settings;
|
||||
|
||||
pub use window_settings::WindowSettings;
|
||||
|
||||
use winit::event_loop::EventLoopWindowTarget;
|
||||
|
||||
#[cfg(feature = "wayland")]
|
||||
#[cfg(any(
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd"
|
||||
))]
|
||||
use winit::platform::unix::EventLoopWindowTargetExtUnix;
|
||||
|
||||
pub fn native_pixels_per_point(window: &winit::window::Window) -> f32 {
|
||||
window.scale_factor() as f32
|
||||
}
|
||||
@@ -71,7 +64,6 @@ pub struct State {
|
||||
current_pixels_per_point: f32,
|
||||
|
||||
clipboard: clipboard::Clipboard,
|
||||
screen_reader: screen_reader::ScreenReader,
|
||||
|
||||
/// If `true`, mouse inputs will be treated as touches.
|
||||
/// Useful for debugging touch support in egui.
|
||||
@@ -86,6 +78,9 @@ pub struct State {
|
||||
|
||||
/// track ime state
|
||||
input_method_editor_started: bool,
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
accesskit: Option<accesskit_winit::Adapter>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
@@ -108,15 +103,31 @@ impl State {
|
||||
current_pixels_per_point: 1.0,
|
||||
|
||||
clipboard: clipboard::Clipboard::new(wayland_display),
|
||||
screen_reader: screen_reader::ScreenReader::default(),
|
||||
|
||||
simulate_touch_screen: false,
|
||||
pointer_touch_id: None,
|
||||
|
||||
input_method_editor_started: false,
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
accesskit: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub fn init_accesskit<T: From<accesskit_winit::ActionRequestEvent> + Send>(
|
||||
&mut self,
|
||||
window: &winit::window::Window,
|
||||
event_loop_proxy: winit::event_loop::EventLoopProxy<T>,
|
||||
initial_tree_update_factory: impl 'static + FnOnce() -> accesskit::TreeUpdate + Send,
|
||||
) {
|
||||
self.accesskit = Some(accesskit_winit::Adapter::new(
|
||||
window,
|
||||
initial_tree_update_factory,
|
||||
event_loop_proxy,
|
||||
));
|
||||
}
|
||||
|
||||
/// Call this once a graphics context has been created to update the maximum texture dimensions
|
||||
/// that egui will use.
|
||||
pub fn set_max_texture_side(&mut self, max_texture_side: usize) {
|
||||
@@ -356,8 +367,9 @@ impl State {
|
||||
consumed: false,
|
||||
}
|
||||
}
|
||||
WindowEvent::AxisMotion { .. }
|
||||
| WindowEvent::CloseRequested
|
||||
|
||||
// Things that may require repaint:
|
||||
WindowEvent::CloseRequested
|
||||
| WindowEvent::CursorEntered { .. }
|
||||
| WindowEvent::Destroyed
|
||||
| WindowEvent::Occluded(_)
|
||||
@@ -367,13 +379,39 @@ impl State {
|
||||
repaint: true,
|
||||
consumed: false,
|
||||
},
|
||||
WindowEvent::Moved(_) => EventResponse {
|
||||
repaint: false, // moving a window doesn't warrant a repaint
|
||||
|
||||
// Things we completely ignore:
|
||||
WindowEvent::AxisMotion { .. }
|
||||
| WindowEvent::Moved(_)
|
||||
| WindowEvent::SmartMagnify { .. }
|
||||
| WindowEvent::TouchpadRotate { .. } => EventResponse {
|
||||
repaint: false,
|
||||
consumed: false,
|
||||
},
|
||||
|
||||
WindowEvent::TouchpadMagnify { delta, .. } => {
|
||||
// Positive delta values indicate magnification (zooming in).
|
||||
// Negative delta values indicate shrinking (zooming out).
|
||||
let zoom_factor = (*delta as f32).exp();
|
||||
self.egui_input.events.push(egui::Event::Zoom(zoom_factor));
|
||||
EventResponse {
|
||||
repaint: true,
|
||||
consumed: egui_ctx.wants_pointer_input(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Call this when there is a new [`accesskit::ActionRequest`].
|
||||
///
|
||||
/// The result can be found in [`Self::egui_input`] and be extracted with [`Self::take_egui_input`].
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub fn on_accesskit_action_request(&mut self, request: accesskit::ActionRequest) {
|
||||
self.egui_input
|
||||
.events
|
||||
.push(egui::Event::AccessKitActionRequest(request));
|
||||
}
|
||||
|
||||
fn on_mouse_button_input(
|
||||
&mut self,
|
||||
state: winit::event::ElementState,
|
||||
@@ -560,6 +598,7 @@ impl State {
|
||||
self.egui_input.events.push(egui::Event::Key {
|
||||
key,
|
||||
pressed,
|
||||
repeat: false, // egui will fill this in for us!
|
||||
modifiers: self.egui_input.modifiers,
|
||||
});
|
||||
}
|
||||
@@ -580,11 +619,6 @@ impl State {
|
||||
egui_ctx: &egui::Context,
|
||||
platform_output: egui::PlatformOutput,
|
||||
) {
|
||||
if egui_ctx.options().screen_reader {
|
||||
self.screen_reader
|
||||
.speak(&platform_output.events_description());
|
||||
}
|
||||
|
||||
let egui::PlatformOutput {
|
||||
cursor_icon,
|
||||
open_url,
|
||||
@@ -592,6 +626,8 @@ impl State {
|
||||
events: _, // handled above
|
||||
mutable_text_under_cursor: _, // only used in eframe web
|
||||
text_cursor_pos,
|
||||
#[cfg(feature = "accesskit")]
|
||||
accesskit_update,
|
||||
} = platform_output;
|
||||
self.current_pixels_per_point = egui_ctx.pixels_per_point(); // someone can have changed it to scale the UI
|
||||
|
||||
@@ -608,11 +644,18 @@ impl State {
|
||||
if let Some(egui::Pos2 { x, y }) = text_cursor_pos {
|
||||
window.set_ime_position(winit::dpi::LogicalPosition { x, y });
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
if let Some(accesskit) = self.accesskit.as_ref() {
|
||||
if let Some(update) = accesskit_update {
|
||||
accesskit.update_if_active(|| update);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_cursor_icon(&mut self, window: &winit::window::Window, cursor_icon: egui::CursorIcon) {
|
||||
// prevent flickering near frame boundary when Windows OS tries to control cursor icon for window resizing
|
||||
#[cfg(windows)]
|
||||
// Prevent flickering near frame boundary when Windows OS tries to control cursor icon for window resizing.
|
||||
// On other platforms: just early-out to save CPU.
|
||||
if self.current_cursor_icon == cursor_icon {
|
||||
return;
|
||||
}
|
||||
@@ -835,6 +878,7 @@ fn wayland_display<T>(_event_loop: &EventLoopWindowTarget<T>) -> Option<*mut c_v
|
||||
target_os = "openbsd"
|
||||
))]
|
||||
{
|
||||
use winit::platform::wayland::EventLoopWindowTargetExtWayland as _;
|
||||
return _event_loop.wayland_display();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
pub struct ScreenReader {
|
||||
#[cfg(feature = "tts")]
|
||||
tts: Option<tts::Tts>,
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "tts"))]
|
||||
#[allow(clippy::derivable_impls)] // False positive
|
||||
impl Default for ScreenReader {
|
||||
fn default() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tts")]
|
||||
impl Default for ScreenReader {
|
||||
fn default() -> Self {
|
||||
let tts = match tts::Tts::default() {
|
||||
Ok(screen_reader) => {
|
||||
tracing::debug!("Initialized screen reader.");
|
||||
Some(screen_reader)
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to load screen reader: {}", err);
|
||||
None
|
||||
}
|
||||
};
|
||||
Self { tts }
|
||||
}
|
||||
}
|
||||
|
||||
impl ScreenReader {
|
||||
#[cfg(not(feature = "tts"))]
|
||||
#[allow(clippy::unused_self)]
|
||||
pub fn speak(&mut self, _text: &str) {}
|
||||
|
||||
#[cfg(feature = "tts")]
|
||||
pub fn speak(&mut self, text: &str) {
|
||||
if text.is_empty() {
|
||||
return;
|
||||
}
|
||||
if let Some(tts) = &mut self.tts {
|
||||
tracing::debug!("Speaking: {:?}", text);
|
||||
let interrupt = true;
|
||||
if let Err(err) = tts.speak(text, interrupt) {
|
||||
tracing::warn!("Failed to read: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,16 +42,21 @@ impl WindowSettings {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inner_size_points(&self) -> Option<egui::Vec2> {
|
||||
self.inner_size_points
|
||||
}
|
||||
|
||||
pub fn initialize_window(
|
||||
&self,
|
||||
mut window: winit::window::WindowBuilder,
|
||||
) -> winit::window::WindowBuilder {
|
||||
if !cfg!(target_os = "windows") {
|
||||
// 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 is hidden and very difficult to find.
|
||||
// So we don't restore window positions on Windows.
|
||||
// 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 is hidden and very difficult to find.
|
||||
// So we don't restore window positions on Windows.
|
||||
let try_restore_position = !cfg!(target_os = "windows");
|
||||
if try_restore_position {
|
||||
if let Some(pos) = self.position {
|
||||
window = window.with_position(winit::dpi::PhysicalPosition {
|
||||
x: pos.x as f64,
|
||||
@@ -68,10 +73,21 @@ impl WindowSettings {
|
||||
})
|
||||
.with_fullscreen(
|
||||
self.fullscreen
|
||||
.then(|| winit::window::Fullscreen::Borderless(None)),
|
||||
.then_some(winit::window::Fullscreen::Borderless(None)),
|
||||
)
|
||||
} else {
|
||||
window
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clamp_to_sane_values(&mut self, max_size: egui::Vec2) {
|
||||
use egui::NumExt as _;
|
||||
|
||||
if let Some(size) = &mut self.inner_size_points {
|
||||
// Prevent ridiculously small windows
|
||||
let min_size = egui::Vec2::splat(64.0);
|
||||
*size = size.at_least(min_size);
|
||||
*size = size.at_most(max_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
[package]
|
||||
name = "egui"
|
||||
version = "0.19.0"
|
||||
version = "0.20.1"
|
||||
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.62"
|
||||
rust-version = "1.65"
|
||||
homepage = "https://github.com/emilk/egui"
|
||||
license = "MIT OR Apache-2.0"
|
||||
readme = "../../README.md"
|
||||
@@ -52,10 +52,10 @@ mint = ["epaint/mint"]
|
||||
persistence = ["serde", "epaint/serde", "ron"]
|
||||
|
||||
## Allow serialization using [`serde`](https://docs.rs/serde).
|
||||
serde = ["dep:serde", "epaint/serde"]
|
||||
serde = ["dep:serde", "epaint/serde", "accesskit?/serde"]
|
||||
|
||||
[dependencies]
|
||||
epaint = { version = "0.19.0", path = "../epaint", default-features = false }
|
||||
epaint = { version = "0.20.0", path = "../epaint", default-features = false }
|
||||
|
||||
ahash = { version = "0.8.1", default-features = false, features = [
|
||||
"no-rng", # we don't need DOS-protection, so we let users opt-in to it instead
|
||||
@@ -64,6 +64,10 @@ ahash = { version = "0.8.1", default-features = false, features = [
|
||||
nohash-hasher = "0.2"
|
||||
|
||||
#! ### Optional dependencies
|
||||
## Exposes detailed accessibility implementation required by platform
|
||||
## accessibility APIs. Also requires support in the egui integration.
|
||||
accesskit = { version = "0.8.1", optional = true }
|
||||
|
||||
## Enable this when generating docs.
|
||||
document-features = { version = "0.2", optional = true }
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
use crate::*;
|
||||
|
||||
/// State that is persisted between frames.
|
||||
// TODO(emilk): this is not currently stored in `memory().data`, but maybe it should be?
|
||||
// TODO(emilk): this is not currently stored in `Memory::data`, but maybe it should be?
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub(crate) struct State {
|
||||
@@ -192,11 +192,12 @@ pub(crate) struct Prepared {
|
||||
move_response: Response,
|
||||
enabled: bool,
|
||||
drag_bounds: Option<Rect>,
|
||||
/// Set the first frame of new windows with anchors.
|
||||
|
||||
/// We always make windows invisible the first frame to hide "first-frame-jitters".
|
||||
///
|
||||
/// This is so that we use the first frame to calculate the window size,
|
||||
/// and then can correctly position the window the next frame,
|
||||
/// without having one frame where the window is positioned in the wrong place.
|
||||
/// and then can correctly position the window and its contents the next frame,
|
||||
/// without having one frame where the window is wrongly positioned or sized.
|
||||
temporarily_invisible: bool,
|
||||
}
|
||||
|
||||
@@ -230,7 +231,7 @@ impl Area {
|
||||
|
||||
let layer_id = LayerId::new(order, id);
|
||||
|
||||
let state = ctx.memory().areas.get(id).cloned();
|
||||
let state = ctx.memory(|mem| mem.areas.get(id).copied());
|
||||
let is_new = state.is_none();
|
||||
if is_new {
|
||||
ctx.request_repaint(); // if we don't know the previous size we are likely drawing the area in the wrong place
|
||||
@@ -242,24 +243,15 @@ impl Area {
|
||||
});
|
||||
state.pos = new_pos.unwrap_or(state.pos);
|
||||
state.interactable = interactable;
|
||||
let mut temporarily_invisible = false;
|
||||
|
||||
if pivot != Align2::LEFT_TOP {
|
||||
if is_new {
|
||||
temporarily_invisible = true; // figure out the size first
|
||||
} else {
|
||||
state.pos.x -= pivot.x().to_factor() * state.size.x;
|
||||
state.pos.y -= pivot.y().to_factor() * state.size.y;
|
||||
}
|
||||
state.pos.x -= pivot.x().to_factor() * state.size.x;
|
||||
state.pos.y -= pivot.y().to_factor() * state.size.y;
|
||||
}
|
||||
|
||||
if let Some((anchor, offset)) = anchor {
|
||||
if is_new {
|
||||
temporarily_invisible = true; // figure out the size first
|
||||
} else {
|
||||
let screen = ctx.available_rect();
|
||||
state.pos = anchor.align_size_within_rect(state.size, screen).min + offset;
|
||||
}
|
||||
let screen = ctx.available_rect();
|
||||
state.pos = anchor.align_size_within_rect(state.size, screen).min + offset;
|
||||
}
|
||||
|
||||
// interact right away to prevent frame-delay
|
||||
@@ -286,7 +278,7 @@ impl Area {
|
||||
// Important check - don't try to move e.g. a combobox popup!
|
||||
if movable {
|
||||
if move_response.dragged() {
|
||||
state.pos += ctx.input().pointer.delta();
|
||||
state.pos += ctx.input(|i| i.pointer.delta());
|
||||
}
|
||||
|
||||
state.pos = ctx
|
||||
@@ -296,9 +288,9 @@ impl Area {
|
||||
|
||||
if (move_response.dragged() || move_response.clicked())
|
||||
|| pointer_pressed_on_area(ctx, layer_id)
|
||||
|| !ctx.memory().areas.visible_last_frame(&layer_id)
|
||||
|| !ctx.memory(|m| m.areas.visible_last_frame(&layer_id))
|
||||
{
|
||||
ctx.memory().areas.move_to_top(layer_id);
|
||||
ctx.memory_mut(|m| m.areas.move_to_top(layer_id));
|
||||
ctx.request_repaint();
|
||||
}
|
||||
|
||||
@@ -319,7 +311,7 @@ impl Area {
|
||||
move_response,
|
||||
enabled,
|
||||
drag_bounds,
|
||||
temporarily_invisible,
|
||||
temporarily_invisible: is_new,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,7 +329,7 @@ impl Area {
|
||||
}
|
||||
|
||||
let layer_id = LayerId::new(self.order, self.id);
|
||||
let area_rect = ctx.memory().areas.get(self.id).map(|area| area.rect());
|
||||
let area_rect = ctx.memory(|mem| mem.areas.get(self.id).map(|area| area.rect()));
|
||||
if let Some(area_rect) = area_rect {
|
||||
let clip_rect = ctx.available_rect();
|
||||
let painter = Painter::new(ctx.clone(), layer_id, clip_rect);
|
||||
@@ -366,7 +358,7 @@ impl Prepared {
|
||||
}
|
||||
|
||||
pub(crate) fn content_ui(&self, ctx: &Context) -> Ui {
|
||||
let screen_rect = ctx.input().screen_rect();
|
||||
let screen_rect = ctx.screen_rect();
|
||||
|
||||
let bounds = if let Some(bounds) = self.drag_bounds {
|
||||
bounds.intersect(screen_rect) // protect against infinite bounds
|
||||
@@ -418,7 +410,7 @@ impl Prepared {
|
||||
|
||||
state.size = content_ui.min_rect().size();
|
||||
|
||||
ctx.memory().areas.set_state(layer_id, state);
|
||||
ctx.memory_mut(|m| m.areas.set_state(layer_id, state));
|
||||
|
||||
move_response
|
||||
}
|
||||
@@ -426,7 +418,7 @@ impl Prepared {
|
||||
|
||||
fn pointer_pressed_on_area(ctx: &Context, layer_id: LayerId) -> bool {
|
||||
if let Some(pointer_pos) = ctx.pointer_interact_pos() {
|
||||
let any_pressed = ctx.input().pointer.any_pressed();
|
||||
let any_pressed = ctx.input(|i| i.pointer.any_pressed());
|
||||
any_pressed && ctx.layer_id_at(pointer_pos) == Some(layer_id)
|
||||
} else {
|
||||
false
|
||||
@@ -434,13 +426,13 @@ fn pointer_pressed_on_area(ctx: &Context, layer_id: LayerId) -> bool {
|
||||
}
|
||||
|
||||
fn automatic_area_position(ctx: &Context) -> Pos2 {
|
||||
let mut existing: Vec<Rect> = ctx
|
||||
.memory()
|
||||
.areas
|
||||
.visible_windows()
|
||||
.into_iter()
|
||||
.map(State::rect)
|
||||
.collect();
|
||||
let mut existing: Vec<Rect> = ctx.memory(|mem| {
|
||||
mem.areas
|
||||
.visible_windows()
|
||||
.into_iter()
|
||||
.map(State::rect)
|
||||
.collect()
|
||||
});
|
||||
existing.sort_by_key(|r| r.left().round() as i32);
|
||||
|
||||
let available_rect = ctx.available_rect();
|
||||
|
||||
@@ -26,13 +26,14 @@ pub struct CollapsingState {
|
||||
|
||||
impl CollapsingState {
|
||||
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
|
||||
ctx.data()
|
||||
.get_persisted::<InnerState>(id)
|
||||
.map(|state| Self { id, state })
|
||||
ctx.data_mut(|d| {
|
||||
d.get_persisted::<InnerState>(id)
|
||||
.map(|state| Self { id, state })
|
||||
})
|
||||
}
|
||||
|
||||
pub fn store(&self, ctx: &Context) {
|
||||
ctx.data().insert_persisted(self.id, self.state);
|
||||
ctx.data_mut(|d| d.insert_persisted(self.id, self.state));
|
||||
}
|
||||
|
||||
pub fn id(&self) -> Id {
|
||||
@@ -64,7 +65,7 @@ impl CollapsingState {
|
||||
|
||||
/// 0 for closed, 1 for open, with tweening
|
||||
pub fn openness(&self, ctx: &Context) -> f32 {
|
||||
if ctx.memory().everything_is_visible() {
|
||||
if ctx.memory(|mem| mem.everything_is_visible()) {
|
||||
1.0
|
||||
} else {
|
||||
ctx.animate_bool(self.id, self.state.open)
|
||||
@@ -111,10 +112,7 @@ impl CollapsingState {
|
||||
response.rect.center().y,
|
||||
));
|
||||
let openness = self.openness(ui.ctx());
|
||||
let small_icon_response = Response {
|
||||
rect: icon_rect,
|
||||
..response.clone()
|
||||
};
|
||||
let small_icon_response = response.clone().with_new_rect(icon_rect);
|
||||
icon_fn(ui, openness, &small_icon_response);
|
||||
response
|
||||
}
|
||||
@@ -311,7 +309,6 @@ impl<'ui, HeaderRet> HeaderResponse<'ui, HeaderRet> {
|
||||
/// Paint the arrow icon that indicated if the region is open or not
|
||||
pub fn paint_default_icon(ui: &mut Ui, openness: f32, response: &Response) {
|
||||
let visuals = ui.style().interact(response);
|
||||
let stroke = visuals.fg_stroke;
|
||||
|
||||
let rect = response.rect;
|
||||
|
||||
@@ -325,7 +322,11 @@ pub fn paint_default_icon(ui: &mut Ui, openness: f32, response: &Response) {
|
||||
*p = rect.center() + rotation * (*p - rect.center());
|
||||
}
|
||||
|
||||
ui.painter().add(Shape::closed_line(points, stroke));
|
||||
ui.painter().add(Shape::convex_polygon(
|
||||
points,
|
||||
visuals.fg_stroke.color,
|
||||
Stroke::NONE,
|
||||
));
|
||||
}
|
||||
|
||||
/// A function that paints an icon indicating if the region is open or not
|
||||
@@ -552,7 +553,7 @@ impl CollapsingHeader {
|
||||
ui.painter().add(epaint::RectShape {
|
||||
rect: header_response.rect.expand(visuals.expansion),
|
||||
rounding: visuals.rounding,
|
||||
fill: visuals.bg_fill,
|
||||
fill: visuals.weak_bg_fill,
|
||||
stroke: visuals.bg_stroke,
|
||||
// stroke: Default::default(),
|
||||
});
|
||||
@@ -572,10 +573,7 @@ impl CollapsingHeader {
|
||||
header_response.rect.left() + ui.spacing().indent / 2.0,
|
||||
header_response.rect.center().y,
|
||||
));
|
||||
let icon_response = Response {
|
||||
rect: icon_rect,
|
||||
..header_response.clone()
|
||||
};
|
||||
let icon_response = header_response.clone().with_new_rect(icon_rect);
|
||||
if let Some(icon) = icon {
|
||||
icon(ui, openness, &icon_response);
|
||||
} else {
|
||||
|
||||
@@ -36,6 +36,7 @@ pub struct ComboBox {
|
||||
selected_text: WidgetText,
|
||||
width: Option<f32>,
|
||||
icon: Option<IconPainter>,
|
||||
wrap_enabled: bool,
|
||||
}
|
||||
|
||||
impl ComboBox {
|
||||
@@ -47,6 +48,7 @@ impl ComboBox {
|
||||
selected_text: Default::default(),
|
||||
width: None,
|
||||
icon: None,
|
||||
wrap_enabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +61,7 @@ impl ComboBox {
|
||||
selected_text: Default::default(),
|
||||
width: None,
|
||||
icon: None,
|
||||
wrap_enabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,10 +73,11 @@ impl ComboBox {
|
||||
selected_text: Default::default(),
|
||||
width: None,
|
||||
icon: None,
|
||||
wrap_enabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the width of the button and menu
|
||||
/// Set the outer width of the button and menu.
|
||||
pub fn width(mut self, width: f32) -> Self {
|
||||
self.width = Some(width);
|
||||
self
|
||||
@@ -124,6 +128,12 @@ impl ComboBox {
|
||||
self
|
||||
}
|
||||
|
||||
/// Controls whether text wrap is used for the selected text
|
||||
pub fn wrap(mut self, wrap: bool) -> Self {
|
||||
self.wrap_enabled = wrap;
|
||||
self
|
||||
}
|
||||
|
||||
/// Show the combo box, with the given ui code for the menu contents.
|
||||
///
|
||||
/// Returns `InnerResponse { inner: None }` if the combo box is closed.
|
||||
@@ -146,15 +156,21 @@ impl ComboBox {
|
||||
selected_text,
|
||||
width,
|
||||
icon,
|
||||
wrap_enabled,
|
||||
} = self;
|
||||
|
||||
let button_id = ui.make_persistent_id(id_source);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if let Some(width) = width {
|
||||
ui.spacing_mut().slider_width = width; // yes, this is ugly. Will remove later.
|
||||
}
|
||||
let mut ir = combo_box_dyn(ui, button_id, selected_text, menu_contents, icon);
|
||||
let mut ir = combo_box_dyn(
|
||||
ui,
|
||||
button_id,
|
||||
selected_text,
|
||||
menu_contents,
|
||||
icon,
|
||||
wrap_enabled,
|
||||
width,
|
||||
);
|
||||
if let Some(label) = label {
|
||||
ir.response
|
||||
.widget_info(|| WidgetInfo::labeled(WidgetType::ComboBox, label.text()));
|
||||
@@ -221,35 +237,59 @@ fn combo_box_dyn<'c, R>(
|
||||
selected_text: WidgetText,
|
||||
menu_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
|
||||
icon: Option<IconPainter>,
|
||||
wrap_enabled: bool,
|
||||
width: Option<f32>,
|
||||
) -> InnerResponse<Option<R>> {
|
||||
let popup_id = button_id.with("popup");
|
||||
|
||||
let is_popup_open = ui.memory().is_popup_open(popup_id);
|
||||
let is_popup_open = ui.memory(|m| m.is_popup_open(popup_id));
|
||||
|
||||
let popup_height = ui
|
||||
.ctx()
|
||||
.memory()
|
||||
.areas
|
||||
.get(popup_id)
|
||||
.map_or(100.0, |state| state.size.y);
|
||||
let popup_height = ui.memory(|m| m.areas.get(popup_id).map_or(100.0, |state| state.size.y));
|
||||
|
||||
let above_or_below =
|
||||
if ui.next_widget_position().y + ui.spacing().interact_size.y + popup_height
|
||||
< ui.ctx().input().screen_rect().bottom()
|
||||
< ui.ctx().screen_rect().bottom()
|
||||
{
|
||||
AboveOrBelow::Below
|
||||
} else {
|
||||
AboveOrBelow::Above
|
||||
};
|
||||
|
||||
let margin = ui.spacing().button_padding;
|
||||
let button_response = button_frame(ui, button_id, is_popup_open, Sense::click(), |ui| {
|
||||
let icon_spacing = ui.spacing().icon_spacing;
|
||||
// We don't want to change width when user selects something new
|
||||
let full_minimum_width = ui.spacing().slider_width;
|
||||
let full_minimum_width = if wrap_enabled {
|
||||
// Currently selected value's text will be wrapped if needed, so occupy the available width.
|
||||
ui.available_width()
|
||||
} else {
|
||||
// Occupy at least the minimum width assigned to ComboBox.
|
||||
let width = width.unwrap_or_else(|| ui.spacing().combo_width);
|
||||
width - 2.0 * margin.x
|
||||
};
|
||||
let icon_size = Vec2::splat(ui.spacing().icon_width);
|
||||
let wrap_width = if wrap_enabled {
|
||||
// Use the available width, currently selected value's text will be wrapped if exceeds this value.
|
||||
ui.available_width() - icon_spacing - icon_size.x
|
||||
} else {
|
||||
// Use all the width necessary to display the currently selected value's text.
|
||||
f32::INFINITY
|
||||
};
|
||||
|
||||
let galley = selected_text.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Button);
|
||||
let galley =
|
||||
selected_text.into_galley(ui, Some(wrap_enabled), wrap_width, TextStyle::Button);
|
||||
|
||||
let width = galley.size().x + ui.spacing().item_spacing.x + icon_size.x;
|
||||
// The width necessary to contain the whole widget with the currently selected value's text.
|
||||
let width = if wrap_enabled {
|
||||
full_minimum_width
|
||||
} else {
|
||||
// Occupy at least the minimum width needed to contain the widget with the currently selected value's text.
|
||||
galley.size().x + icon_spacing + icon_size.x
|
||||
};
|
||||
|
||||
// Case : wrap_enabled : occupy all the available width.
|
||||
// Case : !wrap_enabled : occupy at least the minimum width assigned to Slider and ComboBox,
|
||||
// increase if the currently selected value needs additional horizontal space to fully display its text (up to wrap_width (f32::INFINITY)).
|
||||
let width = width.at_least(full_minimum_width);
|
||||
let height = galley.size().y.max(icon_size.y);
|
||||
|
||||
@@ -289,7 +329,7 @@ fn combo_box_dyn<'c, R>(
|
||||
});
|
||||
|
||||
if button_response.clicked() {
|
||||
ui.memory().toggle_popup(popup_id);
|
||||
ui.memory_mut(|mem| mem.toggle_popup(popup_id));
|
||||
}
|
||||
let inner = crate::popup::popup_above_or_below_widget(
|
||||
ui,
|
||||
@@ -346,7 +386,7 @@ fn button_frame(
|
||||
epaint::RectShape {
|
||||
rect: outer_rect.expand(visuals.expansion),
|
||||
rounding: visuals.rounding,
|
||||
fill: visuals.bg_fill,
|
||||
fill: visuals.weak_bg_fill,
|
||||
stroke: visuals.bg_stroke,
|
||||
},
|
||||
);
|
||||
@@ -371,16 +411,18 @@ fn paint_default_icon(
|
||||
match above_or_below {
|
||||
AboveOrBelow::Above => {
|
||||
// Upward pointing triangle
|
||||
painter.add(Shape::closed_line(
|
||||
painter.add(Shape::convex_polygon(
|
||||
vec![rect.left_bottom(), rect.right_bottom(), rect.center_top()],
|
||||
visuals.fg_stroke,
|
||||
visuals.fg_stroke.color,
|
||||
Stroke::NONE,
|
||||
));
|
||||
}
|
||||
AboveOrBelow::Below => {
|
||||
// Downward pointing triangle
|
||||
painter.add(Shape::closed_line(
|
||||
painter.add(Shape::convex_polygon(
|
||||
vec![rect.left_top(), rect.right_top(), rect.center_bottom()],
|
||||
visuals.fg_stroke,
|
||||
visuals.fg_stroke.color,
|
||||
Stroke::NONE,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,18 +42,18 @@ impl Frame {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn side_top_panel(style: &Style) -> Self {
|
||||
pub fn side_top_panel(style: &Style) -> Self {
|
||||
Self {
|
||||
inner_margin: Margin::symmetric(8.0, 2.0),
|
||||
fill: style.visuals.window_fill(),
|
||||
fill: style.visuals.panel_fill,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn central_panel(style: &Style) -> Self {
|
||||
pub fn central_panel(style: &Style) -> Self {
|
||||
Self {
|
||||
inner_margin: Margin::same(8.0),
|
||||
fill: style.visuals.window_fill(),
|
||||
fill: style.visuals.panel_fill,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,7 @@ impl Frame {
|
||||
pub fn menu(style: &Style) -> Self {
|
||||
Self {
|
||||
inner_margin: style.spacing.menu_margin,
|
||||
rounding: style.visuals.widgets.noninteractive.rounding,
|
||||
rounding: style.visuals.menu_rounding,
|
||||
shadow: style.visuals.popup_shadow,
|
||||
fill: style.visuals.window_fill(),
|
||||
stroke: style.visuals.window_stroke(),
|
||||
@@ -82,8 +82,8 @@ impl Frame {
|
||||
|
||||
pub fn popup(style: &Style) -> Self {
|
||||
Self {
|
||||
inner_margin: style.spacing.window_margin,
|
||||
rounding: style.visuals.widgets.noninteractive.rounding,
|
||||
inner_margin: style.spacing.menu_margin,
|
||||
rounding: style.visuals.menu_rounding,
|
||||
shadow: style.visuals.popup_shadow,
|
||||
fill: style.visuals.window_fill(),
|
||||
stroke: style.visuals.window_stroke(),
|
||||
|
||||
@@ -28,7 +28,7 @@ pub struct PanelState {
|
||||
|
||||
impl PanelState {
|
||||
pub fn load(ctx: &Context, bar_id: Id) -> Option<Self> {
|
||||
ctx.data().get_persisted(bar_id)
|
||||
ctx.data_mut(|d| d.get_persisted(bar_id))
|
||||
}
|
||||
|
||||
/// The size of the panel (from previous frame).
|
||||
@@ -37,14 +37,14 @@ impl PanelState {
|
||||
}
|
||||
|
||||
fn store(self, ctx: &Context, bar_id: Id) {
|
||||
ctx.data().insert_persisted(bar_id, self);
|
||||
ctx.data_mut(|d| d.insert_persisted(bar_id, self));
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// [`Left`](Side::Left) or [`Right`](Side::Right)
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Side {
|
||||
Left,
|
||||
Right,
|
||||
@@ -97,6 +97,7 @@ pub struct SidePanel {
|
||||
id: Id,
|
||||
frame: Option<Frame>,
|
||||
resizable: bool,
|
||||
show_separator_line: bool,
|
||||
default_width: f32,
|
||||
width_range: RangeInclusive<f32>,
|
||||
}
|
||||
@@ -119,6 +120,7 @@ impl SidePanel {
|
||||
id: id.into(),
|
||||
frame: None,
|
||||
resizable: true,
|
||||
show_separator_line: true,
|
||||
default_width: 200.0,
|
||||
width_range: 96.0..=f32::INFINITY,
|
||||
}
|
||||
@@ -140,6 +142,14 @@ impl SidePanel {
|
||||
self
|
||||
}
|
||||
|
||||
/// Show a separator line, even when not interacting with it?
|
||||
///
|
||||
/// Default: `true`.
|
||||
pub fn show_separator_line(mut self, show_separator_line: bool) -> Self {
|
||||
self.show_separator_line = show_separator_line;
|
||||
self
|
||||
}
|
||||
|
||||
/// The initial wrapping width of the [`SidePanel`].
|
||||
pub fn default_width(mut self, default_width: f32) -> Self {
|
||||
self.default_width = default_width;
|
||||
@@ -202,6 +212,7 @@ impl SidePanel {
|
||||
id,
|
||||
frame,
|
||||
resizable,
|
||||
show_separator_line,
|
||||
default_width,
|
||||
width_range,
|
||||
} = self;
|
||||
@@ -215,6 +226,7 @@ impl SidePanel {
|
||||
}
|
||||
width = clamp_to_range(width, width_range.clone()).at_most(available_rect.width());
|
||||
side.set_rect_width(&mut panel_rect, width);
|
||||
ui.ctx().check_for_id_clash(id, panel_rect, "SidePanel");
|
||||
}
|
||||
|
||||
let mut resize_hover = false;
|
||||
@@ -233,11 +245,12 @@ impl SidePanel {
|
||||
&& (resize_x - pointer.x).abs()
|
||||
<= ui.style().interaction.resize_grab_radius_side;
|
||||
|
||||
let any_pressed = ui.input().pointer.any_pressed(); // avoid deadlocks
|
||||
if any_pressed && ui.input().pointer.any_down() && mouse_over_resize_line {
|
||||
ui.memory().set_dragged_id(resize_id);
|
||||
if ui.input(|i| i.pointer.any_pressed() && i.pointer.any_down())
|
||||
&& mouse_over_resize_line
|
||||
{
|
||||
ui.memory_mut(|mem| mem.set_dragged_id(resize_id));
|
||||
}
|
||||
is_resizing = ui.memory().is_being_dragged(resize_id);
|
||||
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 =
|
||||
@@ -245,12 +258,12 @@ impl SidePanel {
|
||||
side.set_rect_width(&mut panel_rect, width);
|
||||
}
|
||||
|
||||
let any_down = ui.input().pointer.any_down(); // avoid deadlocks
|
||||
let dragging_something_else = any_down || ui.input().pointer.any_pressed();
|
||||
let dragging_something_else =
|
||||
ui.input(|i| i.pointer.any_down() || i.pointer.any_pressed());
|
||||
resize_hover = mouse_over_resize_line && !dragging_something_else;
|
||||
|
||||
if resize_hover || is_resizing {
|
||||
ui.output().cursor_icon = CursorIcon::ResizeHorizontal;
|
||||
ui.ctx().set_cursor_icon(CursorIcon::ResizeHorizontal);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -284,15 +297,20 @@ impl SidePanel {
|
||||
|
||||
{
|
||||
let stroke = if is_resizing {
|
||||
ui.style().visuals.widgets.active.bg_stroke
|
||||
ui.style().visuals.widgets.active.fg_stroke // highly visible
|
||||
} else if resize_hover {
|
||||
ui.style().visuals.widgets.hovered.bg_stroke
|
||||
} else {
|
||||
ui.style().visuals.widgets.hovered.fg_stroke // highly visible
|
||||
} else if show_separator_line {
|
||||
// TOOD(emilk): distinguish resizable from non-resizable
|
||||
ui.style().visuals.widgets.noninteractive.bg_stroke
|
||||
ui.style().visuals.widgets.noninteractive.bg_stroke // dim
|
||||
} else {
|
||||
Stroke::NONE
|
||||
};
|
||||
// TODO(emilk): draw line on top of all panels in this ui when https://github.com/emilk/egui/issues/1516 is done
|
||||
let resize_x = side.opposite().side_x(rect);
|
||||
// In the meantime: nudge the line so its inside the panel, so it won't be covered by neighboring panel
|
||||
// (hence the shrink).
|
||||
let resize_x = side.opposite().side_x(rect.shrink(1.0));
|
||||
let resize_x = ui.painter().round_to_pixel(resize_x);
|
||||
ui.painter().vline(resize_x, rect.y_range(), stroke);
|
||||
}
|
||||
|
||||
@@ -317,19 +335,19 @@ impl SidePanel {
|
||||
let layer_id = LayerId::background();
|
||||
let side = self.side;
|
||||
let available_rect = ctx.available_rect();
|
||||
let clip_rect = ctx.input().screen_rect();
|
||||
let clip_rect = ctx.screen_rect();
|
||||
let mut panel_ui = Ui::new(ctx.clone(), layer_id, self.id, available_rect, clip_rect);
|
||||
|
||||
let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents);
|
||||
let rect = inner_response.response.rect;
|
||||
|
||||
match side {
|
||||
Side::Left => ctx
|
||||
.frame_state()
|
||||
.allocate_left_panel(Rect::from_min_max(available_rect.min, rect.max)),
|
||||
Side::Right => ctx
|
||||
.frame_state()
|
||||
.allocate_right_panel(Rect::from_min_max(rect.min, available_rect.max)),
|
||||
Side::Left => ctx.frame_state_mut(|state| {
|
||||
state.allocate_left_panel(Rect::from_min_max(available_rect.min, rect.max));
|
||||
}),
|
||||
Side::Right => ctx.frame_state_mut(|state| {
|
||||
state.allocate_right_panel(Rect::from_min_max(rect.min, available_rect.max));
|
||||
}),
|
||||
}
|
||||
inner_response
|
||||
}
|
||||
@@ -348,6 +366,8 @@ impl SidePanel {
|
||||
None
|
||||
} else if how_expanded < 1.0 {
|
||||
// Show a fake panel in this in-between animation state:
|
||||
// TODO(emilk): move the panel out-of-screen instead of changing its width.
|
||||
// Then we can actually paint it as it animates.
|
||||
let expanded_width = PanelState::load(ctx, self.id)
|
||||
.map_or(self.default_width, |state| state.rect.width());
|
||||
let fake_width = how_expanded * expanded_width;
|
||||
@@ -381,6 +401,8 @@ impl SidePanel {
|
||||
None
|
||||
} else if how_expanded < 1.0 {
|
||||
// Show a fake panel in this in-between animation state:
|
||||
// TODO(emilk): move the panel out-of-screen instead of changing its width.
|
||||
// Then we can actually paint it as it animates.
|
||||
let expanded_width = PanelState::load(ui.ctx(), self.id)
|
||||
.map_or(self.default_width, |state| state.rect.width());
|
||||
let fake_width = how_expanded * expanded_width;
|
||||
@@ -467,7 +489,7 @@ impl SidePanel {
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// [`Top`](TopBottomSide::Top) or [`Bottom`](TopBottomSide::Bottom)
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum TopBottomSide {
|
||||
Top,
|
||||
Bottom,
|
||||
@@ -520,6 +542,7 @@ pub struct TopBottomPanel {
|
||||
id: Id,
|
||||
frame: Option<Frame>,
|
||||
resizable: bool,
|
||||
show_separator_line: bool,
|
||||
default_height: Option<f32>,
|
||||
height_range: RangeInclusive<f32>,
|
||||
}
|
||||
@@ -542,6 +565,7 @@ impl TopBottomPanel {
|
||||
id: id.into(),
|
||||
frame: None,
|
||||
resizable: false,
|
||||
show_separator_line: true,
|
||||
default_height: None,
|
||||
height_range: 20.0..=f32::INFINITY,
|
||||
}
|
||||
@@ -563,6 +587,14 @@ impl TopBottomPanel {
|
||||
self
|
||||
}
|
||||
|
||||
/// Show a separator line, even when not interacting with it?
|
||||
///
|
||||
/// Default: `true`.
|
||||
pub fn show_separator_line(mut self, show_separator_line: bool) -> Self {
|
||||
self.show_separator_line = show_separator_line;
|
||||
self
|
||||
}
|
||||
|
||||
/// The initial height of the [`SidePanel`].
|
||||
/// Defaults to [`style::Spacing::interact_size`].y.
|
||||
pub fn default_height(mut self, default_height: f32) -> Self {
|
||||
@@ -628,6 +660,7 @@ impl TopBottomPanel {
|
||||
id,
|
||||
frame,
|
||||
resizable,
|
||||
show_separator_line,
|
||||
default_height,
|
||||
height_range,
|
||||
} = self;
|
||||
@@ -642,13 +675,15 @@ impl TopBottomPanel {
|
||||
};
|
||||
height = clamp_to_range(height, height_range.clone()).at_most(available_rect.height());
|
||||
side.set_rect_height(&mut panel_rect, height);
|
||||
ui.ctx()
|
||||
.check_for_id_clash(id, panel_rect, "TopBottomPanel");
|
||||
}
|
||||
|
||||
let mut resize_hover = false;
|
||||
let mut is_resizing = false;
|
||||
if resizable {
|
||||
let resize_id = id.with("__resize");
|
||||
let latest_pos = ui.input().pointer.latest_pos();
|
||||
let latest_pos = ui.input(|i| i.pointer.latest_pos());
|
||||
if let Some(pointer) = latest_pos {
|
||||
let we_are_on_top = ui
|
||||
.ctx()
|
||||
@@ -661,13 +696,12 @@ impl TopBottomPanel {
|
||||
&& (resize_y - pointer.y).abs()
|
||||
<= ui.style().interaction.resize_grab_radius_side;
|
||||
|
||||
if ui.input().pointer.any_pressed()
|
||||
&& ui.input().pointer.any_down()
|
||||
if ui.input(|i| i.pointer.any_pressed() && i.pointer.any_down())
|
||||
&& mouse_over_resize_line
|
||||
{
|
||||
ui.memory().interaction.drag_id = Some(resize_id);
|
||||
ui.memory_mut(|mem| mem.interaction.drag_id = Some(resize_id));
|
||||
}
|
||||
is_resizing = ui.memory().interaction.drag_id == Some(resize_id);
|
||||
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())
|
||||
@@ -675,12 +709,12 @@ impl TopBottomPanel {
|
||||
side.set_rect_height(&mut panel_rect, height);
|
||||
}
|
||||
|
||||
let any_down = ui.input().pointer.any_down(); // avoid deadlocks
|
||||
let dragging_something_else = any_down || ui.input().pointer.any_pressed();
|
||||
let dragging_something_else =
|
||||
ui.input(|i| i.pointer.any_down() || i.pointer.any_pressed());
|
||||
resize_hover = mouse_over_resize_line && !dragging_something_else;
|
||||
|
||||
if resize_hover || is_resizing {
|
||||
ui.output().cursor_icon = CursorIcon::ResizeVertical;
|
||||
ui.ctx().set_cursor_icon(CursorIcon::ResizeVertical);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -714,15 +748,20 @@ impl TopBottomPanel {
|
||||
|
||||
{
|
||||
let stroke = if is_resizing {
|
||||
ui.style().visuals.widgets.active.bg_stroke
|
||||
ui.style().visuals.widgets.active.fg_stroke // highly visible
|
||||
} else if resize_hover {
|
||||
ui.style().visuals.widgets.hovered.bg_stroke
|
||||
} else {
|
||||
ui.style().visuals.widgets.hovered.fg_stroke // highly visible
|
||||
} else if show_separator_line {
|
||||
// TOOD(emilk): distinguish resizable from non-resizable
|
||||
ui.style().visuals.widgets.noninteractive.bg_stroke
|
||||
ui.style().visuals.widgets.noninteractive.bg_stroke // dim
|
||||
} else {
|
||||
Stroke::NONE
|
||||
};
|
||||
// TODO(emilk): draw line on top of all panels in this ui when https://github.com/emilk/egui/issues/1516 is done
|
||||
let resize_y = side.opposite().side_y(rect);
|
||||
// In the meantime: nudge the line so its inside the panel, so it won't be covered by neighboring panel
|
||||
// (hence the shrink).
|
||||
let resize_y = side.opposite().side_y(rect.shrink(1.0));
|
||||
let resize_y = ui.painter().round_to_pixel(resize_y);
|
||||
ui.painter().hline(rect.x_range(), resize_y, stroke);
|
||||
}
|
||||
|
||||
@@ -748,7 +787,7 @@ impl TopBottomPanel {
|
||||
let available_rect = ctx.available_rect();
|
||||
let side = self.side;
|
||||
|
||||
let clip_rect = ctx.input().screen_rect();
|
||||
let clip_rect = ctx.screen_rect();
|
||||
let mut panel_ui = Ui::new(ctx.clone(), layer_id, self.id, available_rect, clip_rect);
|
||||
|
||||
let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents);
|
||||
@@ -756,12 +795,14 @@ impl TopBottomPanel {
|
||||
|
||||
match side {
|
||||
TopBottomSide::Top => {
|
||||
ctx.frame_state()
|
||||
.allocate_top_panel(Rect::from_min_max(available_rect.min, rect.max));
|
||||
ctx.frame_state_mut(|state| {
|
||||
state.allocate_top_panel(Rect::from_min_max(available_rect.min, rect.max));
|
||||
});
|
||||
}
|
||||
TopBottomSide::Bottom => {
|
||||
ctx.frame_state()
|
||||
.allocate_bottom_panel(Rect::from_min_max(rect.min, available_rect.max));
|
||||
ctx.frame_state_mut(|state| {
|
||||
state.allocate_bottom_panel(Rect::from_min_max(rect.min, available_rect.max));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -782,6 +823,8 @@ impl TopBottomPanel {
|
||||
None
|
||||
} else if how_expanded < 1.0 {
|
||||
// Show a fake panel in this in-between animation state:
|
||||
// TODO(emilk): move the panel out-of-screen instead of changing its height.
|
||||
// Then we can actually paint it as it animates.
|
||||
let expanded_height = PanelState::load(ctx, self.id)
|
||||
.map(|state| state.rect.height())
|
||||
.or(self.default_height)
|
||||
@@ -817,6 +860,8 @@ impl TopBottomPanel {
|
||||
None
|
||||
} else if how_expanded < 1.0 {
|
||||
// Show a fake panel in this in-between animation state:
|
||||
// TODO(emilk): move the panel out-of-screen instead of changing its height.
|
||||
// Then we can actually paint it as it animates.
|
||||
let expanded_height = PanelState::load(ui.ctx(), self.id)
|
||||
.map(|state| state.rect.height())
|
||||
.or(self.default_height)
|
||||
@@ -999,14 +1044,13 @@ impl CentralPanel {
|
||||
let layer_id = LayerId::background();
|
||||
let id = Id::new("central_panel");
|
||||
|
||||
let clip_rect = ctx.input().screen_rect();
|
||||
let clip_rect = ctx.screen_rect();
|
||||
let mut panel_ui = Ui::new(ctx.clone(), layer_id, id, available_rect, clip_rect);
|
||||
|
||||
let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents);
|
||||
|
||||
// Only inform ctx about what we actually used, so we can shrink the native window to fit.
|
||||
ctx.frame_state()
|
||||
.allocate_central_panel(inner_response.response.rect);
|
||||
ctx.frame_state_mut(|state| state.allocate_central_panel(inner_response.response.rect));
|
||||
|
||||
inner_response
|
||||
}
|
||||
|
||||
@@ -13,16 +13,16 @@ pub(crate) struct TooltipState {
|
||||
|
||||
impl TooltipState {
|
||||
pub fn load(ctx: &Context) -> Option<Self> {
|
||||
ctx.data().get_temp(Id::null())
|
||||
ctx.data_mut(|d| d.get_temp(Id::null()))
|
||||
}
|
||||
|
||||
fn store(self, ctx: &Context) {
|
||||
ctx.data().insert_temp(Id::null(), self);
|
||||
ctx.data_mut(|d| d.insert_temp(Id::null(), self));
|
||||
}
|
||||
|
||||
fn individual_tooltip_size(&self, common_id: Id, index: usize) -> Option<Vec2> {
|
||||
if self.last_common_id == Some(common_id) {
|
||||
Some(self.individual_ids_and_sizes.get(&index).cloned()?.1)
|
||||
Some(self.individual_ids_and_sizes.get(&index)?.1)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -95,9 +95,7 @@ pub fn show_tooltip_at_pointer<R>(
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> Option<R> {
|
||||
let suggested_pos = ctx
|
||||
.input()
|
||||
.pointer
|
||||
.hover_pos()
|
||||
.input(|i| i.pointer.hover_pos())
|
||||
.map(|pointer_pos| pointer_pos + vec2(16.0, 16.0));
|
||||
show_tooltip_at(ctx, id, suggested_pos, add_contents)
|
||||
}
|
||||
@@ -112,7 +110,7 @@ pub fn show_tooltip_for<R>(
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> Option<R> {
|
||||
let expanded_rect = rect.expand2(vec2(2.0, 4.0));
|
||||
let (above, position) = if ctx.input().any_touches() {
|
||||
let (above, position) = if ctx.input(|i| i.any_touches()) {
|
||||
(true, expanded_rect.left_top())
|
||||
} else {
|
||||
(false, expanded_rect.left_bottom())
|
||||
@@ -159,8 +157,7 @@ fn show_tooltip_at_avoid_dyn<'c, R>(
|
||||
|
||||
// if there are multiple tooltips open they should use the same common_id for the `tooltip_size` caching to work.
|
||||
let mut frame_state =
|
||||
ctx.frame_state()
|
||||
.tooltip_state
|
||||
ctx.frame_state(|fs| fs.tooltip_state)
|
||||
.unwrap_or(crate::frame_state::TooltipFrameState {
|
||||
common_id: individual_id,
|
||||
rect: Rect::NOTHING,
|
||||
@@ -176,7 +173,7 @@ fn show_tooltip_at_avoid_dyn<'c, R>(
|
||||
}
|
||||
} else if let Some(position) = suggested_position {
|
||||
position
|
||||
} else if ctx.memory().everything_is_visible() {
|
||||
} else if ctx.memory(|mem| mem.everything_is_visible()) {
|
||||
Pos2::ZERO
|
||||
} else {
|
||||
return None; // No good place for a tooltip :(
|
||||
@@ -191,7 +188,7 @@ fn show_tooltip_at_avoid_dyn<'c, R>(
|
||||
position.y -= expected_size.y;
|
||||
}
|
||||
|
||||
position = position.at_most(ctx.input().screen_rect().max - expected_size);
|
||||
position = position.at_most(ctx.screen_rect().max - expected_size);
|
||||
|
||||
// check if we intersect the avoid_rect
|
||||
{
|
||||
@@ -209,7 +206,7 @@ fn show_tooltip_at_avoid_dyn<'c, R>(
|
||||
}
|
||||
}
|
||||
|
||||
let position = position.at_least(ctx.input().screen_rect().min);
|
||||
let position = position.at_least(ctx.screen_rect().min);
|
||||
|
||||
let area_id = frame_state.common_id.with(frame_state.count);
|
||||
|
||||
@@ -226,7 +223,7 @@ fn show_tooltip_at_avoid_dyn<'c, R>(
|
||||
|
||||
frame_state.count += 1;
|
||||
frame_state.rect = frame_state.rect.union(response.rect);
|
||||
ctx.frame_state().tooltip_state = Some(frame_state);
|
||||
ctx.frame_state_mut(|fs| fs.tooltip_state = Some(frame_state));
|
||||
|
||||
Some(inner)
|
||||
}
|
||||
@@ -283,7 +280,7 @@ pub fn was_tooltip_open_last_frame(ctx: &Context, tooltip_id: Id) -> bool {
|
||||
if *individual_id == tooltip_id {
|
||||
let area_id = common_id.with(count);
|
||||
let layer_id = LayerId::new(Order::Tooltip, area_id);
|
||||
if ctx.memory().areas.visible_last_frame(&layer_id) {
|
||||
if ctx.memory(|mem| mem.areas.visible_last_frame(&layer_id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -314,6 +311,8 @@ pub fn popup_below_widget<R>(
|
||||
///
|
||||
/// Useful for drop-down menus (combo boxes) or suggestion menus under text fields.
|
||||
///
|
||||
/// The opened popup will have the same width as the parent.
|
||||
///
|
||||
/// You must open the popup with [`Memory::open_popup`] or [`Memory::toggle_popup`].
|
||||
///
|
||||
/// Returns `None` if the popup is not open.
|
||||
@@ -323,7 +322,7 @@ pub fn popup_below_widget<R>(
|
||||
/// let response = ui.button("Open popup");
|
||||
/// let popup_id = ui.make_persistent_id("my_unique_id");
|
||||
/// if response.clicked() {
|
||||
/// ui.memory().toggle_popup(popup_id);
|
||||
/// ui.memory_mut(|mem| mem.toggle_popup(popup_id));
|
||||
/// }
|
||||
/// let below = egui::AboveOrBelow::Below;
|
||||
/// egui::popup::popup_above_or_below_widget(ui, popup_id, &response, below, |ui| {
|
||||
@@ -340,7 +339,7 @@ pub fn popup_above_or_below_widget<R>(
|
||||
above_or_below: AboveOrBelow,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> Option<R> {
|
||||
if ui.memory().is_popup_open(popup_id) {
|
||||
if ui.memory(|mem| mem.is_popup_open(popup_id)) {
|
||||
let (pos, pivot) = match above_or_below {
|
||||
AboveOrBelow::Above => (widget_response.rect.left_top(), Align2::LEFT_BOTTOM),
|
||||
AboveOrBelow::Below => (widget_response.rect.left_bottom(), Align2::LEFT_TOP),
|
||||
@@ -368,8 +367,8 @@ pub fn popup_above_or_below_widget<R>(
|
||||
})
|
||||
.inner;
|
||||
|
||||
if ui.input().key_pressed(Key::Escape) || widget_response.clicked_elsewhere() {
|
||||
ui.memory().close_popup();
|
||||
if ui.input(|i| i.key_pressed(Key::Escape)) || widget_response.clicked_elsewhere() {
|
||||
ui.memory_mut(|mem| mem.close_popup());
|
||||
}
|
||||
Some(inner)
|
||||
} else {
|
||||
|
||||
@@ -18,11 +18,11 @@ pub(crate) struct State {
|
||||
|
||||
impl State {
|
||||
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
|
||||
ctx.data().get_persisted(id)
|
||||
ctx.data_mut(|d| d.get_persisted(id))
|
||||
}
|
||||
|
||||
pub fn store(self, ctx: &Context, id: Id) {
|
||||
ctx.data().insert_persisted(id, self);
|
||||
ctx.data_mut(|d| d.insert_persisted(id, self));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ impl Resize {
|
||||
.at_least(self.min_size)
|
||||
.at_most(self.max_size)
|
||||
.at_most(
|
||||
ui.input().screen_rect().size() - ui.spacing().window_margin.sum(), // hack for windows
|
||||
ui.ctx().screen_rect().size() - ui.spacing().window_margin.sum(), // hack for windows
|
||||
);
|
||||
|
||||
State {
|
||||
@@ -305,7 +305,7 @@ impl Resize {
|
||||
paint_resize_corner(ui, &corner_response);
|
||||
|
||||
if corner_response.hovered() || corner_response.dragged() {
|
||||
ui.ctx().output().cursor_icon = CursorIcon::ResizeNwSe;
|
||||
ui.ctx().set_cursor_icon(CursorIcon::ResizeNwSe);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,11 +48,11 @@ impl Default for State {
|
||||
|
||||
impl State {
|
||||
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
|
||||
ctx.data().get_persisted(id)
|
||||
ctx.data_mut(|d| d.get_persisted(id))
|
||||
}
|
||||
|
||||
pub fn store(self, ctx: &Context, id: Id) {
|
||||
ctx.data().insert_persisted(id, self);
|
||||
ctx.data_mut(|d| d.insert_persisted(id, self));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,10 @@ pub struct ScrollAreaOutput<R> {
|
||||
/// The current state of the scroll area.
|
||||
pub state: State,
|
||||
|
||||
/// The size of the content. If this is larger than [`Self::inner_rect`],
|
||||
/// then there was need for scrolling.
|
||||
pub content_size: Vec2,
|
||||
|
||||
/// Where on the screen the content is (excludes scroll bars).
|
||||
pub inner_rect: Rect,
|
||||
}
|
||||
@@ -93,8 +97,10 @@ pub struct ScrollArea {
|
||||
id_source: Option<Id>,
|
||||
offset_x: Option<f32>,
|
||||
offset_y: Option<f32>,
|
||||
|
||||
/// If false, we ignore scroll events.
|
||||
scrolling_enabled: bool,
|
||||
drag_to_scroll: bool,
|
||||
|
||||
/// If true for vertical or horizontal the scroll wheel will stick to the
|
||||
/// end position until user manually changes position. It will become true
|
||||
@@ -137,6 +143,7 @@ impl ScrollArea {
|
||||
offset_x: None,
|
||||
offset_y: None,
|
||||
scrolling_enabled: true,
|
||||
drag_to_scroll: true,
|
||||
stick_to_end: [false; 2],
|
||||
}
|
||||
}
|
||||
@@ -198,6 +205,8 @@ impl ScrollArea {
|
||||
|
||||
/// Set the horizontal and vertical scroll offset position.
|
||||
///
|
||||
/// Positive offset means scrolling down/right.
|
||||
///
|
||||
/// See also: [`Self::vertical_scroll_offset`], [`Self::horizontal_scroll_offset`],
|
||||
/// [`Ui::scroll_to_cursor`](crate::ui::Ui::scroll_to_cursor) and
|
||||
/// [`Response::scroll_to_me`](crate::Response::scroll_to_me)
|
||||
@@ -209,6 +218,8 @@ impl ScrollArea {
|
||||
|
||||
/// Set the vertical scroll offset position.
|
||||
///
|
||||
/// Positive offset means scrolling down.
|
||||
///
|
||||
/// See also: [`Self::scroll_offset`], [`Ui::scroll_to_cursor`](crate::ui::Ui::scroll_to_cursor) and
|
||||
/// [`Response::scroll_to_me`](crate::Response::scroll_to_me)
|
||||
pub fn vertical_scroll_offset(mut self, offset: f32) -> Self {
|
||||
@@ -218,6 +229,8 @@ impl ScrollArea {
|
||||
|
||||
/// Set the horizontal scroll offset position.
|
||||
///
|
||||
/// Positive offset means scrolling right.
|
||||
///
|
||||
/// See also: [`Self::scroll_offset`], [`Ui::scroll_to_cursor`](crate::ui::Ui::scroll_to_cursor) and
|
||||
/// [`Response::scroll_to_me`](crate::Response::scroll_to_me)
|
||||
pub fn horizontal_scroll_offset(mut self, offset: f32) -> Self {
|
||||
@@ -243,12 +256,13 @@ impl ScrollArea {
|
||||
self
|
||||
}
|
||||
|
||||
/// Control the scrolling behavior
|
||||
/// If `true` (default), the scroll area will respond to user scrolling
|
||||
/// If `false`, the scroll area will not respond to user scrolling
|
||||
/// Control the scrolling behavior.
|
||||
///
|
||||
/// * If `true` (default), the scroll area will respond to user scrolling.
|
||||
/// * If `false`, the scroll area will not respond to user scrolling.
|
||||
///
|
||||
/// This can be used, for example, to optionally freeze scrolling while the user
|
||||
/// is inputing text in a [`TextEdit`] widget contained within the scroll area.
|
||||
/// is typing text in a [`TextEdit`] widget contained within the scroll area.
|
||||
///
|
||||
/// This controls both scrolling directions.
|
||||
pub fn enable_scrolling(mut self, enable: bool) -> Self {
|
||||
@@ -256,10 +270,22 @@ impl ScrollArea {
|
||||
self
|
||||
}
|
||||
|
||||
/// Can the user drag the scroll area to scroll?
|
||||
///
|
||||
/// This is useful for touch screens.
|
||||
///
|
||||
/// If `true`, the [`ScrollArea`] will sense drags.
|
||||
///
|
||||
/// Default: `true`.
|
||||
pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self {
|
||||
self.drag_to_scroll = drag_to_scroll;
|
||||
self
|
||||
}
|
||||
|
||||
/// For each axis, should the containing area shrink if the content is small?
|
||||
///
|
||||
/// If true, egui will add blank space outside the scroll area.
|
||||
/// If false, egui will add blank space inside the scroll area.
|
||||
/// * If `true`, egui will add blank space outside the scroll area.
|
||||
/// * If `false`, egui will add blank space inside the scroll area.
|
||||
///
|
||||
/// Default: `[true; 2]`.
|
||||
pub fn auto_shrink(mut self, auto_shrink: [bool; 2]) -> Self {
|
||||
@@ -325,6 +351,7 @@ impl ScrollArea {
|
||||
offset_x,
|
||||
offset_y,
|
||||
scrolling_enabled,
|
||||
drag_to_scroll,
|
||||
stick_to_end,
|
||||
} = self;
|
||||
|
||||
@@ -399,19 +426,40 @@ impl ScrollArea {
|
||||
|
||||
let content_max_rect = Rect::from_min_size(inner_rect.min - state.offset, content_max_size);
|
||||
let mut content_ui = ui.child_ui(content_max_rect, *ui.layout());
|
||||
let mut content_clip_rect = inner_rect.expand(ui.visuals().clip_rect_margin);
|
||||
content_clip_rect = content_clip_rect.intersect(ui.clip_rect());
|
||||
// Nice handling of forced resizing beyond the possible:
|
||||
for d in 0..2 {
|
||||
if !has_bar[d] {
|
||||
content_clip_rect.max[d] = ui.clip_rect().max[d] - current_bar_use[d];
|
||||
|
||||
{
|
||||
// Clip the content, but only when we really need to:
|
||||
let clip_rect_margin = ui.visuals().clip_rect_margin;
|
||||
let scroll_bar_inner_margin = ui.spacing().scroll_bar_inner_margin;
|
||||
let mut content_clip_rect = ui.clip_rect();
|
||||
for d in 0..2 {
|
||||
if has_bar[d] {
|
||||
if state.content_is_too_large[d] {
|
||||
content_clip_rect.min[d] = inner_rect.min[d] - clip_rect_margin;
|
||||
content_clip_rect.max[d] = inner_rect.max[d] + clip_rect_margin;
|
||||
}
|
||||
|
||||
if state.show_scroll[d] {
|
||||
// Make sure content doesn't cover scroll bars
|
||||
let tiny_gap = 1.0;
|
||||
content_clip_rect.max[1 - d] =
|
||||
inner_rect.max[1 - d] + scroll_bar_inner_margin - tiny_gap;
|
||||
}
|
||||
} else {
|
||||
// Nice handling of forced resizing beyond the possible:
|
||||
content_clip_rect.max[d] = ui.clip_rect().max[d] - current_bar_use[d];
|
||||
}
|
||||
}
|
||||
// Make sure we din't accidentally expand the clip rect
|
||||
content_clip_rect = content_clip_rect.intersect(ui.clip_rect());
|
||||
content_ui.set_clip_rect(content_clip_rect);
|
||||
}
|
||||
content_ui.set_clip_rect(content_clip_rect);
|
||||
|
||||
let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size);
|
||||
|
||||
if scrolling_enabled && (state.content_is_too_large[0] || state.content_is_too_large[1]) {
|
||||
if (scrolling_enabled && drag_to_scroll)
|
||||
&& (state.content_is_too_large[0] || state.content_is_too_large[1])
|
||||
{
|
||||
// Drag contents to scroll (for touch screens mostly).
|
||||
// We must do this BEFORE adding content to the `ScrollArea`,
|
||||
// or we will steal input from the widgets we contain.
|
||||
@@ -420,8 +468,10 @@ impl ScrollArea {
|
||||
if content_response.dragged() {
|
||||
for d in 0..2 {
|
||||
if has_bar[d] {
|
||||
state.offset[d] -= ui.input().pointer.delta()[d];
|
||||
state.vel[d] = ui.input().pointer.velocity()[d];
|
||||
ui.input(|input| {
|
||||
state.offset[d] -= input.pointer.delta()[d];
|
||||
state.vel[d] = input.pointer.velocity()[d];
|
||||
});
|
||||
state.scroll_stuck_to_end[d] = false;
|
||||
} else {
|
||||
state.vel[d] = 0.0;
|
||||
@@ -430,7 +480,7 @@ impl ScrollArea {
|
||||
} else {
|
||||
let stop_speed = 20.0; // Pixels per second.
|
||||
let friction_coeff = 1000.0; // Pixels per second squared.
|
||||
let dt = ui.input().unstable_dt;
|
||||
let dt = ui.input(|i| i.unstable_dt);
|
||||
|
||||
let friction = friction_coeff * dt;
|
||||
if friction > state.vel.length() || state.vel.length() < stop_speed {
|
||||
@@ -541,18 +591,20 @@ impl ScrollArea {
|
||||
let id = prepared.id;
|
||||
let inner_rect = prepared.inner_rect;
|
||||
let inner = add_contents(&mut prepared.content_ui, prepared.viewport);
|
||||
let state = prepared.end(ui);
|
||||
let (content_size, state) = prepared.end(ui);
|
||||
ScrollAreaOutput {
|
||||
inner,
|
||||
id,
|
||||
state,
|
||||
content_size,
|
||||
inner_rect,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Prepared {
|
||||
fn end(self, ui: &mut Ui) -> State {
|
||||
/// Returns content size and state
|
||||
fn end(self, ui: &mut Ui) -> (Vec2, State) {
|
||||
let Prepared {
|
||||
id,
|
||||
mut state,
|
||||
@@ -572,7 +624,9 @@ impl Prepared {
|
||||
for d in 0..2 {
|
||||
if has_bar[d] {
|
||||
// We take the scroll target so only this ScrollArea will use it:
|
||||
let scroll_target = content_ui.ctx().frame_state().scroll_target[d].take();
|
||||
let scroll_target = content_ui
|
||||
.ctx()
|
||||
.frame_state_mut(|state| state.scroll_target[d].take());
|
||||
if let Some((scroll, align)) = scroll_target {
|
||||
let min = content_ui.min_rect().min[d];
|
||||
let clip_rect = content_ui.clip_rect();
|
||||
@@ -623,23 +677,7 @@ impl Prepared {
|
||||
};
|
||||
}
|
||||
|
||||
let mut inner_rect = Rect::from_min_size(inner_rect.min, inner_size);
|
||||
|
||||
// The window that egui sits in can't be expanded by egui, so we need to respect it:
|
||||
for d in 0..2 {
|
||||
if !has_bar[d] {
|
||||
// HACK for when we have a vertical-only scroll area in a top level panel,
|
||||
// and that panel is not wide enough for the contents.
|
||||
// This code ensures we still see the scroll bar!
|
||||
let max = ui.input().screen_rect().max[d]
|
||||
- current_bar_use[d]
|
||||
- ui.spacing().item_spacing[d];
|
||||
inner_rect.max[d] = inner_rect.max[d].at_most(max);
|
||||
// TODO(emilk): maybe auto-enable horizontal/vertical scrolling if this limit is reached
|
||||
}
|
||||
}
|
||||
|
||||
inner_rect
|
||||
Rect::from_min_size(inner_rect.min, inner_size)
|
||||
};
|
||||
|
||||
let outer_rect = Rect::from_min_size(inner_rect.min, inner_rect.size() + current_bar_use);
|
||||
@@ -653,8 +691,7 @@ impl Prepared {
|
||||
if scrolling_enabled && ui.rect_contains_pointer(outer_rect) {
|
||||
for d in 0..2 {
|
||||
if has_bar[d] {
|
||||
let mut frame_state = ui.ctx().frame_state();
|
||||
let scroll_delta = frame_state.scroll_delta;
|
||||
let scroll_delta = ui.ctx().frame_state(|fs| fs.scroll_delta);
|
||||
|
||||
let scrolling_up = state.offset[d] > 0.0 && scroll_delta[d] > 0.0;
|
||||
let scrolling_down = state.offset[d] < max_offset[d] && scroll_delta[d] < 0.0;
|
||||
@@ -662,7 +699,7 @@ impl Prepared {
|
||||
if scrolling_up || scrolling_down {
|
||||
state.offset[d] -= scroll_delta[d];
|
||||
// Clear scroll delta so no parent scroll will use it.
|
||||
frame_state.scroll_delta[d] = 0.0;
|
||||
ui.ctx().frame_state_mut(|fs| fs.scroll_delta[d] = 0.0);
|
||||
state.scroll_stuck_to_end[d] = false;
|
||||
}
|
||||
}
|
||||
@@ -691,13 +728,29 @@ impl Prepared {
|
||||
continue;
|
||||
}
|
||||
|
||||
// margin between contents and scroll bar
|
||||
let margin = animation_t * ui.spacing().item_spacing.x;
|
||||
let min_cross = inner_rect.max[1 - d] + margin; // left of vertical scroll (d == 1)
|
||||
let max_cross = outer_rect.max[1 - d]; // right of vertical scroll (d == 1)
|
||||
// margin on either side of the scroll bar
|
||||
let inner_margin = animation_t * ui.spacing().scroll_bar_inner_margin;
|
||||
let outer_margin = animation_t * ui.spacing().scroll_bar_outer_margin;
|
||||
let mut min_cross = inner_rect.max[1 - d] + inner_margin; // left of vertical scroll (d == 1)
|
||||
let mut max_cross = outer_rect.max[1 - d] - outer_margin; // right of vertical scroll (d == 1)
|
||||
let min_main = inner_rect.min[d]; // top of vertical scroll (d == 1)
|
||||
let max_main = inner_rect.max[d]; // bottom of vertical scroll (d == 1)
|
||||
|
||||
if ui.clip_rect().max[1 - d] < max_cross + outer_margin {
|
||||
// Move the scrollbar so it is visible. This is needed in some cases.
|
||||
// For instance:
|
||||
// * When we have a vertical-only scroll area in a top level panel,
|
||||
// and that panel is not wide enough for the contents.
|
||||
// * When one ScrollArea is nested inside another, and the outer
|
||||
// is scrolled so that the scroll-bars of the inner ScrollArea (us)
|
||||
// is outside the clip rectangle.
|
||||
// Really this should use the tighter clip_rect that ignores clip_rect_margin, but we don't store that.
|
||||
// clip_rect_margin is quite a hack. It would be nice to get rid of it.
|
||||
let width = max_cross - min_cross;
|
||||
max_cross = ui.clip_rect().max[1 - d] - outer_margin;
|
||||
min_cross = max_cross - width;
|
||||
}
|
||||
|
||||
let outer_scroll_rect = if d == 0 {
|
||||
Rect::from_min_max(
|
||||
pos2(inner_rect.left(), min_cross),
|
||||
@@ -788,7 +841,7 @@ impl Prepared {
|
||||
),
|
||||
)
|
||||
};
|
||||
let min_handle_size = ui.spacing().scroll_bar_width;
|
||||
let min_handle_size = ui.spacing().scroll_handle_min_length;
|
||||
if handle_rect.size()[d] < min_handle_size {
|
||||
handle_rect = Rect::from_center_size(
|
||||
handle_rect.center(),
|
||||
@@ -847,11 +900,13 @@ impl Prepared {
|
||||
|
||||
state.store(ui.ctx(), id);
|
||||
|
||||
state
|
||||
(content_size, state)
|
||||
}
|
||||
}
|
||||
|
||||
/// Width of a vertical scrollbar, or height of a horizontal scroll bar
|
||||
fn max_scroll_bar_width_with_margin(ui: &Ui) -> f32 {
|
||||
ui.spacing().item_spacing.x + ui.spacing().scroll_bar_width
|
||||
ui.spacing().scroll_bar_inner_margin
|
||||
+ ui.spacing().scroll_bar_width
|
||||
+ ui.spacing().scroll_bar_outer_margin
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use super::*;
|
||||
///
|
||||
/// You can customize:
|
||||
/// * title
|
||||
/// * default, minimum, maximum and/or fixed size
|
||||
/// * default, minimum, maximum and/or fixed size, collapsed/expanded
|
||||
/// * if the window has a scroll area (off by default)
|
||||
/// * if the window can be collapsed (minimized) to just the title bar (yes, by default)
|
||||
/// * if there should be a close button (none by default)
|
||||
@@ -30,6 +30,7 @@ pub struct Window<'open> {
|
||||
resize: Resize,
|
||||
scroll: ScrollArea,
|
||||
collapsible: bool,
|
||||
default_open: bool,
|
||||
with_title_bar: bool,
|
||||
}
|
||||
|
||||
@@ -51,6 +52,7 @@ impl<'open> Window<'open> {
|
||||
.default_size([340.0, 420.0]), // Default inner size of a window
|
||||
scroll: ScrollArea::neither(),
|
||||
collapsible: true,
|
||||
default_open: true,
|
||||
with_title_bar: true,
|
||||
}
|
||||
}
|
||||
@@ -77,6 +79,18 @@ impl<'open> Window<'open> {
|
||||
self
|
||||
}
|
||||
|
||||
/// If `false` the window will be non-interactive.
|
||||
pub fn interactable(mut self, interactable: bool) -> Self {
|
||||
self.area = self.area.interactable(interactable);
|
||||
self
|
||||
}
|
||||
|
||||
/// If `false` the window will be immovable.
|
||||
pub fn movable(mut self, movable: bool) -> Self {
|
||||
self.area = self.area.movable(movable);
|
||||
self
|
||||
}
|
||||
|
||||
/// Usage: `Window::new(…).mutate(|w| w.resize = w.resize.auto_expand_width(true))`
|
||||
// TODO(emilk): I'm not sure this is a good interface for this.
|
||||
pub fn mutate(mut self, mutate: impl Fn(&mut Self)) -> Self {
|
||||
@@ -162,6 +176,12 @@ impl<'open> Window<'open> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set initial collapsed state of the window
|
||||
pub fn default_open(mut self, default_open: bool) -> Self {
|
||||
self.default_open = default_open;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set initial size of the window.
|
||||
pub fn default_size(mut self, default_size: impl Into<Vec2>) -> Self {
|
||||
self.resize = self.resize.default_size(default_size);
|
||||
@@ -275,12 +295,14 @@ impl<'open> Window<'open> {
|
||||
resize,
|
||||
scroll,
|
||||
collapsible,
|
||||
default_open,
|
||||
with_title_bar,
|
||||
} = self;
|
||||
|
||||
let frame = frame.unwrap_or_else(|| Frame::window(&ctx.style()));
|
||||
|
||||
let is_open = !matches!(open, Some(false)) || ctx.memory().everything_is_visible();
|
||||
let is_explicitly_closed = matches!(open, Some(false));
|
||||
let is_open = !is_explicitly_closed || ctx.memory(|mem| mem.everything_is_visible());
|
||||
area.show_open_close_animation(ctx, &frame, is_open);
|
||||
|
||||
if !is_open {
|
||||
@@ -291,7 +313,7 @@ impl<'open> Window<'open> {
|
||||
let area_layer_id = area.layer();
|
||||
let resize_id = area_id.with("resize");
|
||||
let mut collapsing =
|
||||
CollapsingState::load_with_default_open(ctx, area_id.with("collapsing"), true);
|
||||
CollapsingState::load_with_default_open(ctx, area_id.with("collapsing"), default_open);
|
||||
|
||||
let is_collapsed = with_title_bar && !collapsing.is_open();
|
||||
let possible = PossibleInteractions::new(&area, &resize, is_collapsed);
|
||||
@@ -318,7 +340,7 @@ impl<'open> Window<'open> {
|
||||
// Calculate roughly how much larger the window size is compared to the inner rect
|
||||
let title_bar_height = if with_title_bar {
|
||||
let style = ctx.style();
|
||||
title.font_height(&ctx.fonts(), &style) + title_content_spacing
|
||||
ctx.fonts(|f| title.font_height(f, &style)) + title_content_spacing
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
@@ -404,7 +426,7 @@ impl<'open> Window<'open> {
|
||||
ctx.style().visuals.widgets.active,
|
||||
);
|
||||
} else if let Some(hover_interaction) = hover_interaction {
|
||||
if ctx.input().pointer.has_pointer() {
|
||||
if ctx.input(|i| i.pointer.has_pointer()) {
|
||||
paint_frame_interaction(
|
||||
&mut area_content_ui,
|
||||
outer_rect,
|
||||
@@ -499,13 +521,13 @@ pub(crate) struct WindowInteraction {
|
||||
impl WindowInteraction {
|
||||
pub fn set_cursor(&self, ctx: &Context) {
|
||||
if (self.left && self.top) || (self.right && self.bottom) {
|
||||
ctx.output().cursor_icon = CursorIcon::ResizeNwSe;
|
||||
ctx.set_cursor_icon(CursorIcon::ResizeNwSe);
|
||||
} else if (self.right && self.top) || (self.left && self.bottom) {
|
||||
ctx.output().cursor_icon = CursorIcon::ResizeNeSw;
|
||||
ctx.set_cursor_icon(CursorIcon::ResizeNeSw);
|
||||
} else if self.left || self.right {
|
||||
ctx.output().cursor_icon = CursorIcon::ResizeHorizontal;
|
||||
ctx.set_cursor_icon(CursorIcon::ResizeHorizontal);
|
||||
} else if self.bottom || self.top {
|
||||
ctx.output().cursor_icon = CursorIcon::ResizeVertical;
|
||||
ctx.set_cursor_icon(CursorIcon::ResizeVertical);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -537,7 +559,7 @@ fn interact(
|
||||
}
|
||||
}
|
||||
|
||||
ctx.memory().areas.move_to_top(area_layer_id);
|
||||
ctx.memory_mut(|mem| mem.areas.move_to_top(area_layer_id));
|
||||
Some(window_interaction)
|
||||
}
|
||||
|
||||
@@ -545,11 +567,11 @@ fn move_and_resize_window(ctx: &Context, window_interaction: &WindowInteraction)
|
||||
window_interaction.set_cursor(ctx);
|
||||
|
||||
// Only move/resize windows with primary mouse button:
|
||||
if !ctx.input().pointer.primary_down() {
|
||||
if !ctx.input(|i| i.pointer.primary_down()) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let pointer_pos = ctx.input().pointer.interact_pos()?;
|
||||
let pointer_pos = ctx.input(|i| i.pointer.interact_pos())?;
|
||||
let mut rect = window_interaction.start_rect; // prevent drift
|
||||
|
||||
if window_interaction.is_resize() {
|
||||
@@ -571,8 +593,8 @@ fn move_and_resize_window(ctx: &Context, window_interaction: &WindowInteraction)
|
||||
// but we want anything interactive in the window (e.g. slider) to steal
|
||||
// the drag from us. It is therefor important not to move the window the first frame,
|
||||
// but instead let other widgets to the steal. HACK.
|
||||
if !ctx.input().pointer.any_pressed() {
|
||||
let press_origin = ctx.input().pointer.press_origin()?;
|
||||
if !ctx.input(|i| i.pointer.any_pressed()) {
|
||||
let press_origin = ctx.input(|i| i.pointer.press_origin())?;
|
||||
let delta = pointer_pos - press_origin;
|
||||
rect = rect.translate(delta);
|
||||
}
|
||||
@@ -590,30 +612,31 @@ fn window_interaction(
|
||||
rect: Rect,
|
||||
) -> Option<WindowInteraction> {
|
||||
{
|
||||
let drag_id = ctx.memory().interaction.drag_id;
|
||||
let drag_id = ctx.memory(|mem| mem.interaction.drag_id);
|
||||
|
||||
if drag_id.is_some() && drag_id != Some(id) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
let mut window_interaction = { ctx.memory().window_interaction };
|
||||
let mut window_interaction = ctx.memory(|mem| mem.window_interaction);
|
||||
|
||||
if window_interaction.is_none() {
|
||||
if let Some(hover_window_interaction) = resize_hover(ctx, possible, area_layer_id, rect) {
|
||||
hover_window_interaction.set_cursor(ctx);
|
||||
let any_pressed = ctx.input().pointer.any_pressed(); // avoid deadlocks
|
||||
if any_pressed && ctx.input().pointer.primary_down() {
|
||||
ctx.memory().interaction.drag_id = Some(id);
|
||||
ctx.memory().interaction.drag_is_window = true;
|
||||
window_interaction = Some(hover_window_interaction);
|
||||
ctx.memory().window_interaction = window_interaction;
|
||||
if ctx.input(|i| i.pointer.any_pressed() && i.pointer.primary_down()) {
|
||||
ctx.memory_mut(|mem| {
|
||||
mem.interaction.drag_id = Some(id);
|
||||
mem.interaction.drag_is_window = true;
|
||||
window_interaction = Some(hover_window_interaction);
|
||||
mem.window_interaction = window_interaction;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(window_interaction) = window_interaction {
|
||||
let is_active = ctx.memory().interaction.drag_id == Some(id);
|
||||
let is_active = ctx.memory_mut(|mem| mem.interaction.drag_id == Some(id));
|
||||
|
||||
if is_active && window_interaction.area_layer_id == area_layer_id {
|
||||
return Some(window_interaction);
|
||||
@@ -629,10 +652,9 @@ fn resize_hover(
|
||||
area_layer_id: LayerId,
|
||||
rect: Rect,
|
||||
) -> Option<WindowInteraction> {
|
||||
let pointer = ctx.input().pointer.interact_pos()?;
|
||||
let pointer = ctx.input(|i| i.pointer.interact_pos())?;
|
||||
|
||||
let any_down = ctx.input().pointer.any_down(); // avoid deadlocks
|
||||
if any_down && !ctx.input().pointer.any_pressed() {
|
||||
if ctx.input(|i| i.pointer.any_down() && !i.pointer.any_pressed()) {
|
||||
return None; // already dragging (something)
|
||||
}
|
||||
|
||||
@@ -642,7 +664,7 @@ fn resize_hover(
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.memory().interaction.drag_interest {
|
||||
if ctx.memory(|mem| mem.interaction.drag_interest) {
|
||||
// Another widget will become active if we drag here
|
||||
return None;
|
||||
}
|
||||
@@ -804,8 +826,8 @@ fn show_title_bar(
|
||||
collapsible: bool,
|
||||
) -> TitleBar {
|
||||
let inner_response = ui.horizontal(|ui| {
|
||||
let height = title
|
||||
.font_height(&ui.fonts(), ui.style())
|
||||
let height = ui
|
||||
.fonts(|fonts| title.font_height(fonts, ui.style()))
|
||||
.max(ui.spacing().interact_size.y);
|
||||
ui.set_min_height(height);
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -133,7 +133,7 @@ impl RawInput {
|
||||
}
|
||||
|
||||
/// A file about to be dropped into egui.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct HoveredFile {
|
||||
/// Set by the `egui-winit` backend.
|
||||
@@ -144,7 +144,7 @@ pub struct HoveredFile {
|
||||
}
|
||||
|
||||
/// A file dropped into egui.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct DroppedFile {
|
||||
/// Set by the `egui-winit` backend.
|
||||
@@ -187,6 +187,15 @@ pub enum Event {
|
||||
/// Was it pressed or released?
|
||||
pressed: bool,
|
||||
|
||||
/// If this is a `pressed` event, is it a key-repeat?
|
||||
///
|
||||
/// On many platforms, holding down a key produces many repeated "pressed" events for it, so called key-repeats.
|
||||
/// Sometimes you will want to ignore such events, and this lets you do that.
|
||||
///
|
||||
/// egui will automatically detect such repeat events and mark them as such here.
|
||||
/// Therefore, if you are writing an egui integration, you do not need to set this (just set it to `false`).
|
||||
repeat: bool,
|
||||
|
||||
/// The state of the modifier keys at the time of the event.
|
||||
modifiers: Modifiers,
|
||||
},
|
||||
@@ -268,6 +277,10 @@ pub enum Event {
|
||||
/// The value is in the range from 0.0 (no pressure) to 1.0 (maximum pressure).
|
||||
force: f32,
|
||||
},
|
||||
|
||||
/// An assistive technology (e.g. screen reader) requested an action.
|
||||
#[cfg(feature = "accesskit")]
|
||||
AccessKitActionRequest(accesskit::ActionRequest),
|
||||
}
|
||||
|
||||
/// Mouse button (or similar for touch input)
|
||||
@@ -301,7 +314,7 @@ pub const NUM_POINTER_BUTTONS: usize = 5;
|
||||
/// NOTE: For cross-platform uses, ALT+SHIFT is a bad combination of modifiers
|
||||
/// as on mac that is how you type special characters,
|
||||
/// so those key presses are usually not reported to egui.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct Modifiers {
|
||||
/// Either of the alt keys are down (option ⌥ on Mac).
|
||||
@@ -546,7 +559,7 @@ impl<'a> ModifierNames<'a> {
|
||||
append_if(modifiers.alt, self.alt);
|
||||
append_if(modifiers.mac_cmd || modifiers.command, self.mac_cmd);
|
||||
} else {
|
||||
append_if(modifiers.ctrl, self.ctrl);
|
||||
append_if(modifiers.ctrl || modifiers.command, self.ctrl);
|
||||
append_if(modifiers.alt, self.alt);
|
||||
append_if(modifiers.shift, self.shift);
|
||||
}
|
||||
@@ -764,7 +777,7 @@ impl Key {
|
||||
///
|
||||
/// Can be used with [`crate::InputState::consume_shortcut`]
|
||||
/// and [`crate::Context::format_shortcut`].
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||
pub struct KeyboardShortcut {
|
||||
pub modifiers: Modifiers,
|
||||
pub key: Key,
|
||||
@@ -789,6 +802,21 @@ impl KeyboardShortcut {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_kb_shortcut() {
|
||||
let cmd_shift_f = KeyboardShortcut::new(Modifiers::COMMAND | Modifiers::SHIFT, Key::F);
|
||||
assert_eq!(
|
||||
cmd_shift_f.format(&ModifierNames::NAMES, false),
|
||||
"Ctrl+Shift+F"
|
||||
);
|
||||
assert_eq!(
|
||||
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, true), "⇧⌘F");
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
impl RawInput {
|
||||
|
||||
@@ -70,7 +70,7 @@ pub struct PlatformOutput {
|
||||
/// ```
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
/// if ui.button("📋").clicked() {
|
||||
/// ui.output().copied_text = "some_text".to_string();
|
||||
/// ui.output_mut(|o| o.copied_text = "some_text".to_string());
|
||||
/// }
|
||||
/// # });
|
||||
/// ```
|
||||
@@ -85,6 +85,9 @@ pub struct PlatformOutput {
|
||||
|
||||
/// Screen-space position of text edit cursor (used for IME).
|
||||
pub text_cursor_pos: Option<crate::Pos2>,
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub accesskit_update: Option<accesskit::TreeUpdate>,
|
||||
}
|
||||
|
||||
impl PlatformOutput {
|
||||
@@ -121,6 +124,8 @@ impl PlatformOutput {
|
||||
mut events,
|
||||
mutable_text_under_cursor,
|
||||
text_cursor_pos,
|
||||
#[cfg(feature = "accesskit")]
|
||||
accesskit_update,
|
||||
} = newer;
|
||||
|
||||
self.cursor_icon = cursor_icon;
|
||||
@@ -133,6 +138,13 @@ impl PlatformOutput {
|
||||
self.events.append(&mut events);
|
||||
self.mutable_text_under_cursor = mutable_text_under_cursor;
|
||||
self.text_cursor_pos = text_cursor_pos.or(self.text_cursor_pos);
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
{
|
||||
// egui produces a complete AccessKit tree for each frame,
|
||||
// so overwrite rather than appending.
|
||||
self.accesskit_update = accesskit_update;
|
||||
}
|
||||
}
|
||||
|
||||
/// Take everything ephemeral (everything except `cursor_icon` currently)
|
||||
@@ -144,7 +156,7 @@ impl PlatformOutput {
|
||||
}
|
||||
|
||||
/// What URL to open, and how.
|
||||
#[derive(Clone, PartialEq)]
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct OpenUrl {
|
||||
pub url: String,
|
||||
@@ -178,7 +190,7 @@ impl OpenUrl {
|
||||
/// egui emits a [`CursorIcon`] in [`PlatformOutput`] each frame as a request to the integration.
|
||||
///
|
||||
/// Loosely based on <https://developer.mozilla.org/en-US/docs/Web/CSS/cursor>.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub enum CursorIcon {
|
||||
/// Normal cursor icon, whatever that is.
|
||||
@@ -372,6 +384,19 @@ pub enum OutputEvent {
|
||||
ValueChanged(WidgetInfo),
|
||||
}
|
||||
|
||||
impl OutputEvent {
|
||||
pub fn widget_info(&self) -> &WidgetInfo {
|
||||
match self {
|
||||
OutputEvent::Clicked(info)
|
||||
| OutputEvent::DoubleClicked(info)
|
||||
| OutputEvent::TripleClicked(info)
|
||||
| OutputEvent::FocusGained(info)
|
||||
| OutputEvent::TextSelectionChanged(info)
|
||||
| OutputEvent::ValueChanged(info) => info,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for OutputEvent {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
use crate::*;
|
||||
use crate::{id::IdSet, *};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct TooltipFrameState {
|
||||
@@ -9,6 +9,13 @@ pub(crate) struct TooltipFrameState {
|
||||
pub count: usize,
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct AccessKitFrameState {
|
||||
pub(crate) nodes: IdMap<Box<accesskit::Node>>,
|
||||
pub(crate) parent_stack: Vec<Id>,
|
||||
}
|
||||
|
||||
/// State that is collected during a frame and then cleared.
|
||||
/// Short-term (single frame) memory.
|
||||
#[derive(Clone)]
|
||||
@@ -41,6 +48,15 @@ pub(crate) struct FrameState {
|
||||
|
||||
/// horizontal, vertical
|
||||
pub(crate) scroll_target: [Option<(RangeInclusive<f32>, Option<Align>)>; 2],
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub(crate) accesskit_state: Option<AccessKitFrameState>,
|
||||
|
||||
/// Highlight these widgets this next frame. Read from this.
|
||||
pub(crate) highlight_this_frame: IdSet,
|
||||
|
||||
/// Highlight these widgets the next frame. Write to this.
|
||||
pub(crate) highlight_next_frame: IdSet,
|
||||
}
|
||||
|
||||
impl Default for FrameState {
|
||||
@@ -53,6 +69,10 @@ impl Default for FrameState {
|
||||
tooltip_state: None,
|
||||
scroll_delta: Vec2::ZERO,
|
||||
scroll_target: [None, None],
|
||||
#[cfg(feature = "accesskit")]
|
||||
accesskit_state: None,
|
||||
highlight_this_frame: Default::default(),
|
||||
highlight_next_frame: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,6 +87,10 @@ impl FrameState {
|
||||
tooltip_state,
|
||||
scroll_delta,
|
||||
scroll_target,
|
||||
#[cfg(feature = "accesskit")]
|
||||
accesskit_state,
|
||||
highlight_this_frame,
|
||||
highlight_next_frame,
|
||||
} = self;
|
||||
|
||||
used_ids.clear();
|
||||
@@ -76,6 +100,13 @@ impl FrameState {
|
||||
*tooltip_state = None;
|
||||
*scroll_delta = input.scroll_delta;
|
||||
*scroll_target = [None, None];
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
{
|
||||
*accesskit_state = None;
|
||||
}
|
||||
|
||||
*highlight_this_frame = std::mem::take(highlight_next_frame);
|
||||
}
|
||||
|
||||
/// How much space is still available after panels has been added.
|
||||
|
||||
@@ -8,14 +8,14 @@ pub(crate) struct State {
|
||||
|
||||
impl State {
|
||||
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
|
||||
ctx.data().get_temp(id)
|
||||
ctx.data_mut(|d| d.get_temp(id))
|
||||
}
|
||||
|
||||
pub fn store(self, ctx: &Context, id: Id) {
|
||||
// We don't persist Grids, because
|
||||
// A) there are potentially a lot of them, using up a lot of space (and therefore serialization time)
|
||||
// B) if the code changes, the grid _should_ change, and not remember old sizes
|
||||
ctx.data().insert_temp(id, self);
|
||||
ctx.data_mut(|d| d.insert_temp(id, self));
|
||||
}
|
||||
|
||||
fn set_min_col_width(&mut self, col: usize, width: f32) {
|
||||
@@ -51,6 +51,9 @@ pub(crate) struct GridLayout {
|
||||
style: std::sync::Arc<Style>,
|
||||
id: Id,
|
||||
|
||||
/// First frame (no previous know state).
|
||||
is_first_frame: bool,
|
||||
|
||||
/// State previous frame (if any).
|
||||
/// This can be used to predict future sizes of cells.
|
||||
prev_state: State,
|
||||
@@ -71,8 +74,9 @@ pub(crate) struct GridLayout {
|
||||
}
|
||||
|
||||
impl GridLayout {
|
||||
pub(crate) fn new(ui: &Ui, id: Id) -> Self {
|
||||
let prev_state = State::load(ui.ctx(), id).unwrap_or_default();
|
||||
pub(crate) fn new(ui: &Ui, id: Id, prev_state: Option<State>) -> Self {
|
||||
let is_first_frame = prev_state.is_none();
|
||||
let prev_state = prev_state.unwrap_or_default();
|
||||
|
||||
// TODO(emilk): respect current layout
|
||||
|
||||
@@ -88,6 +92,7 @@ impl GridLayout {
|
||||
ctx: ui.ctx().clone(),
|
||||
style: ui.style().clone(),
|
||||
id,
|
||||
is_first_frame,
|
||||
prev_state,
|
||||
curr_state: State::default(),
|
||||
initial_available,
|
||||
@@ -125,7 +130,16 @@ impl GridLayout {
|
||||
let is_last_column = Some(self.col + 1) == self.num_columns;
|
||||
|
||||
let width = if is_last_column {
|
||||
(self.initial_available.right() - region.cursor.left()).at_most(self.max_cell_size.x)
|
||||
// The first frame we don't really know the widths of the previous columns,
|
||||
// so returning a big available width here can cause trouble.
|
||||
if self.is_first_frame {
|
||||
self.curr_state
|
||||
.col_width(self.col)
|
||||
.unwrap_or(self.min_cell_size.x)
|
||||
} else {
|
||||
(self.initial_available.right() - region.cursor.left())
|
||||
.at_most(self.max_cell_size.x)
|
||||
}
|
||||
} else if self.max_cell_size.x.is_finite() {
|
||||
// TODO(emilk): should probably heed `prev_state` here too
|
||||
self.max_cell_size.x
|
||||
@@ -264,7 +278,7 @@ impl GridLayout {
|
||||
pub struct Grid {
|
||||
id_source: Id,
|
||||
num_columns: Option<usize>,
|
||||
striped: bool,
|
||||
striped: Option<bool>,
|
||||
min_col_width: Option<f32>,
|
||||
min_row_height: Option<f32>,
|
||||
max_cell_size: Vec2,
|
||||
@@ -278,7 +292,7 @@ impl Grid {
|
||||
Self {
|
||||
id_source: Id::new(id_source),
|
||||
num_columns: None,
|
||||
striped: false,
|
||||
striped: None,
|
||||
min_col_width: None,
|
||||
min_row_height: None,
|
||||
max_cell_size: Vec2::INFINITY,
|
||||
@@ -296,9 +310,9 @@ impl Grid {
|
||||
/// If `true`, add a subtle background color to every other row.
|
||||
///
|
||||
/// This can make a table easier to read.
|
||||
/// Default: `false`.
|
||||
/// Default is whatever is in [`crate::Visuals::striped`].
|
||||
pub fn striped(mut self, striped: bool) -> Self {
|
||||
self.striped = striped;
|
||||
self.striped = Some(striped);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -357,18 +371,22 @@ impl Grid {
|
||||
spacing,
|
||||
start_row,
|
||||
} = self;
|
||||
let striped = striped.unwrap_or(ui.visuals().striped);
|
||||
let min_col_width = min_col_width.unwrap_or_else(|| ui.spacing().interact_size.x);
|
||||
let min_row_height = min_row_height.unwrap_or_else(|| ui.spacing().interact_size.y);
|
||||
let spacing = spacing.unwrap_or_else(|| ui.spacing().item_spacing);
|
||||
|
||||
let id = ui.make_persistent_id(id_source);
|
||||
let prev_state = State::load(ui.ctx(), id);
|
||||
|
||||
// Each grid cell is aligned LEFT_CENTER.
|
||||
// If somebody wants to wrap more things inside a cell,
|
||||
// then we should pick a default layout that matches that alignment,
|
||||
// which we do here:
|
||||
let max_rect = ui.cursor().intersect(ui.max_rect());
|
||||
ui.allocate_ui_at_rect(max_rect, |ui| {
|
||||
ui.set_visible(prev_state.is_some()); // Avoid visible first-frame jitter
|
||||
ui.horizontal(|ui| {
|
||||
let id = ui.make_persistent_id(id_source);
|
||||
let grid = GridLayout {
|
||||
num_columns,
|
||||
striped,
|
||||
@@ -376,7 +394,7 @@ impl Grid {
|
||||
max_cell_size,
|
||||
spacing,
|
||||
row: start_row,
|
||||
..GridLayout::new(ui, id)
|
||||
..GridLayout::new(ui, id, prev_state)
|
||||
};
|
||||
|
||||
ui.set_grid(grid);
|
||||
|
||||
@@ -26,15 +26,15 @@ pub mod kb_shortcuts {
|
||||
/// }
|
||||
/// ```
|
||||
pub fn zoom_with_keyboard_shortcuts(ctx: &Context, native_pixels_per_point: Option<f32>) {
|
||||
if ctx.input_mut().consume_shortcut(&kb_shortcuts::ZOOM_RESET) {
|
||||
if ctx.input_mut(|i| i.consume_shortcut(&kb_shortcuts::ZOOM_RESET)) {
|
||||
if let Some(native_pixels_per_point) = native_pixels_per_point {
|
||||
ctx.set_pixels_per_point(native_pixels_per_point);
|
||||
}
|
||||
} else {
|
||||
if ctx.input_mut().consume_shortcut(&kb_shortcuts::ZOOM_IN) {
|
||||
if ctx.input_mut(|i| i.consume_shortcut(&kb_shortcuts::ZOOM_IN)) {
|
||||
zoom_in(ctx);
|
||||
}
|
||||
if ctx.input_mut().consume_shortcut(&kb_shortcuts::ZOOM_OUT) {
|
||||
if ctx.input_mut(|i| i.consume_shortcut(&kb_shortcuts::ZOOM_OUT)) {
|
||||
zoom_out(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +69,11 @@ impl Id {
|
||||
pub(crate) fn value(&self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub(crate) fn accesskit_id(&self) -> accesskit::NodeId {
|
||||
std::num::NonZeroU64::new(self.0).unwrap().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Id {
|
||||
@@ -85,6 +90,13 @@ impl From<&'static str> for Id {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Id {
|
||||
#[inline]
|
||||
fn from(string: String) -> Self {
|
||||
Self::new(string)
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// Idea taken from the `nohash_hasher` crate.
|
||||
@@ -156,5 +168,8 @@ impl std::hash::BuildHasher for BuilIdHasher {
|
||||
}
|
||||
}
|
||||
|
||||
/// `IdSet` is a `HashSet<Id>` optimized by knowing that [`Id`] has good entropy, and doesn't need more hashing.
|
||||
pub type IdSet = std::collections::HashSet<Id, BuilIdHasher>;
|
||||
|
||||
/// `IdMap<V>` is a `HashMap<Id, V>` optimized by knowing that [`Id`] has good entropy, and doesn't need more hashing.
|
||||
pub type IdMap<V> = std::collections::HashMap<Id, V, BuilIdHasher>;
|
||||
|
||||
@@ -138,7 +138,11 @@ impl Default for InputState {
|
||||
|
||||
impl InputState {
|
||||
#[must_use]
|
||||
pub fn begin_frame(mut self, new: RawInput, requested_repaint_last_frame: bool) -> InputState {
|
||||
pub fn begin_frame(
|
||||
mut self,
|
||||
mut new: RawInput,
|
||||
requested_repaint_last_frame: bool,
|
||||
) -> InputState {
|
||||
let time = new.time.unwrap_or(self.time + new.predicted_dt as f64);
|
||||
let unstable_dt = (time - self.time) as f32;
|
||||
|
||||
@@ -160,11 +164,17 @@ impl InputState {
|
||||
let mut keys_down = self.keys_down;
|
||||
let mut scroll_delta = Vec2::ZERO;
|
||||
let mut zoom_factor_delta = 1.0;
|
||||
for event in &new.events {
|
||||
for event in &mut new.events {
|
||||
match event {
|
||||
Event::Key { key, pressed, .. } => {
|
||||
Event::Key {
|
||||
key,
|
||||
pressed,
|
||||
repeat,
|
||||
..
|
||||
} => {
|
||||
if *pressed {
|
||||
keys_down.insert(*key);
|
||||
let first_press = keys_down.insert(*key);
|
||||
*repeat = !first_press;
|
||||
} else {
|
||||
keys_down.remove(key);
|
||||
}
|
||||
@@ -178,6 +188,7 @@ impl InputState {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
InputState {
|
||||
pointer,
|
||||
touch_states: self.touch_states,
|
||||
@@ -245,9 +256,11 @@ impl InputState {
|
||||
self.pointer.wants_repaint() || self.scroll_delta != Vec2::ZERO || !self.events.is_empty()
|
||||
}
|
||||
|
||||
/// Check for a key press. If found, `true` is returned and the key pressed is consumed, so that this will only return `true` once.
|
||||
pub fn consume_key(&mut self, modifiers: Modifiers, key: Key) -> bool {
|
||||
let mut match_found = false;
|
||||
/// Count presses of a key. If non-zero, the presses are consumed, so that this will only return non-zero once.
|
||||
///
|
||||
/// Includes key-repeat events.
|
||||
pub fn count_and_consume_key(&mut self, modifiers: Modifiers, key: Key) -> usize {
|
||||
let mut count = 0usize;
|
||||
|
||||
self.events.retain(|event| {
|
||||
let is_match = matches!(
|
||||
@@ -255,43 +268,54 @@ impl InputState {
|
||||
Event::Key {
|
||||
key: ev_key,
|
||||
modifiers: ev_mods,
|
||||
pressed: true
|
||||
pressed: true,
|
||||
..
|
||||
} if *ev_key == key && ev_mods.matches(modifiers)
|
||||
);
|
||||
|
||||
match_found |= is_match;
|
||||
count += is_match as usize;
|
||||
|
||||
!is_match
|
||||
});
|
||||
|
||||
match_found
|
||||
count
|
||||
}
|
||||
|
||||
/// Check for a key press. If found, `true` is returned and the key pressed is consumed, so that this will only return `true` once.
|
||||
///
|
||||
/// Includes key-repeat events.
|
||||
pub fn consume_key(&mut self, modifiers: Modifiers, key: Key) -> bool {
|
||||
self.count_and_consume_key(modifiers, key) > 0
|
||||
}
|
||||
|
||||
/// Check if the given shortcut has been pressed.
|
||||
///
|
||||
/// If so, `true` is returned and the key pressed is consumed, so that this will only return `true` once.
|
||||
///
|
||||
/// Includes key-repeat events.
|
||||
pub fn consume_shortcut(&mut self, shortcut: &KeyboardShortcut) -> bool {
|
||||
let KeyboardShortcut { modifiers, key } = *shortcut;
|
||||
self.consume_key(modifiers, key)
|
||||
}
|
||||
|
||||
/// Was the given key pressed this frame?
|
||||
///
|
||||
/// Includes key-repeat events.
|
||||
pub fn key_pressed(&self, desired_key: Key) -> bool {
|
||||
self.num_presses(desired_key) > 0
|
||||
}
|
||||
|
||||
/// How many times were the given key pressed this frame?
|
||||
/// How many times was the given key pressed this frame?
|
||||
///
|
||||
/// Includes key-repeat events.
|
||||
pub fn num_presses(&self, desired_key: Key) -> usize {
|
||||
self.events
|
||||
.iter()
|
||||
.filter(|event| {
|
||||
matches!(
|
||||
event,
|
||||
Event::Key {
|
||||
key,
|
||||
pressed: true,
|
||||
..
|
||||
} if *key == desired_key
|
||||
Event::Key { key, pressed: true, .. }
|
||||
if *key == desired_key
|
||||
)
|
||||
})
|
||||
.count()
|
||||
@@ -344,7 +368,7 @@ impl InputState {
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
/// let mut zoom = 1.0; // no zoom
|
||||
/// let mut rotation = 0.0; // no rotation
|
||||
/// let multi_touch = ui.input().multi_touch();
|
||||
/// let multi_touch = ui.input(|i| i.multi_touch());
|
||||
/// if let Some(multi_touch) = multi_touch {
|
||||
/// zoom *= multi_touch.zoom_delta;
|
||||
/// rotation += multi_touch.rotation_delta;
|
||||
@@ -388,6 +412,33 @@ impl InputState {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub fn accesskit_action_requests(
|
||||
&self,
|
||||
id: crate::Id,
|
||||
action: accesskit::Action,
|
||||
) -> impl Iterator<Item = &accesskit::ActionRequest> {
|
||||
let accesskit_id = id.accesskit_id();
|
||||
self.events.iter().filter_map(move |event| {
|
||||
if let Event::AccessKitActionRequest(request) = event {
|
||||
if request.target == accesskit_id && request.action == action {
|
||||
return Some(request);
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub fn has_accesskit_action_request(&self, id: crate::Id, action: accesskit::Action) -> bool {
|
||||
self.accesskit_action_requests(id, action).next().is_some()
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub fn num_accesskit_action_requests(&self, id: crate::Id, action: accesskit::Action) -> usize {
|
||||
self.accesskit_action_requests(id, action).count()
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -396,7 +447,6 @@ impl InputState {
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) struct Click {
|
||||
pub pos: Pos2,
|
||||
pub button: PointerButton,
|
||||
/// 1 or 2 (double-click) or 3 (triple-click)
|
||||
pub count: u32,
|
||||
/// Allows you to check for e.g. shift-click
|
||||
@@ -420,7 +470,10 @@ pub(crate) enum PointerEvent {
|
||||
position: Pos2,
|
||||
button: PointerButton,
|
||||
},
|
||||
Released(Option<Click>),
|
||||
Released {
|
||||
click: Option<Click>,
|
||||
button: PointerButton,
|
||||
},
|
||||
}
|
||||
|
||||
impl PointerEvent {
|
||||
@@ -429,11 +482,11 @@ impl PointerEvent {
|
||||
}
|
||||
|
||||
pub fn is_release(&self) -> bool {
|
||||
matches!(self, PointerEvent::Released(_))
|
||||
matches!(self, PointerEvent::Released { .. })
|
||||
}
|
||||
|
||||
pub fn is_click(&self) -> bool {
|
||||
matches!(self, PointerEvent::Released(Some(_click)))
|
||||
matches!(self, PointerEvent::Released { click: Some(_), .. })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -588,7 +641,6 @@ impl PointerState {
|
||||
|
||||
Some(Click {
|
||||
pos,
|
||||
button,
|
||||
count,
|
||||
modifiers,
|
||||
})
|
||||
@@ -596,7 +648,8 @@ impl PointerState {
|
||||
None
|
||||
};
|
||||
|
||||
self.pointer_events.push(PointerEvent::Released(click));
|
||||
self.pointer_events
|
||||
.push(PointerEvent::Released { click, button });
|
||||
|
||||
self.press_origin = None;
|
||||
self.press_start_time = None;
|
||||
@@ -724,11 +777,28 @@ impl PointerState {
|
||||
self.pointer_events.iter().any(|event| event.is_release())
|
||||
}
|
||||
|
||||
/// Was the button given pressed this frame?
|
||||
pub fn button_pressed(&self, button: PointerButton) -> bool {
|
||||
self.pointer_events
|
||||
.iter()
|
||||
.any(|event| matches!(event, &PointerEvent::Pressed{button: b, ..} if button == b))
|
||||
}
|
||||
|
||||
/// Was the button given released this frame?
|
||||
pub fn button_released(&self, button: PointerButton) -> bool {
|
||||
self.pointer_events
|
||||
.iter()
|
||||
.any(|event| matches!(event, &PointerEvent::Released(Some(Click{button: b, ..})) if button == b))
|
||||
.any(|event| matches!(event, &PointerEvent::Released{button: b, ..} if button == b))
|
||||
}
|
||||
|
||||
/// Was the primary button pressed this frame?
|
||||
pub fn primary_pressed(&self) -> bool {
|
||||
self.button_pressed(PointerButton::Primary)
|
||||
}
|
||||
|
||||
/// Was the secondary button pressed this frame?
|
||||
pub fn secondary_pressed(&self) -> bool {
|
||||
self.button_pressed(PointerButton::Secondary)
|
||||
}
|
||||
|
||||
/// Was the primary button released this frame?
|
||||
@@ -760,16 +830,28 @@ impl PointerState {
|
||||
|
||||
/// Was the button given double clicked this frame?
|
||||
pub fn button_double_clicked(&self, button: PointerButton) -> bool {
|
||||
self.pointer_events
|
||||
.iter()
|
||||
.any(|event| matches!(&event, PointerEvent::Released(Some(click)) if click.button == button && click.is_double()))
|
||||
self.pointer_events.iter().any(|event| {
|
||||
matches!(
|
||||
&event,
|
||||
PointerEvent::Released {
|
||||
click: Some(click),
|
||||
button: b,
|
||||
} if *b == button && click.is_double()
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Was the button given triple clicked this frame?
|
||||
pub fn button_triple_clicked(&self, button: PointerButton) -> bool {
|
||||
self.pointer_events
|
||||
.iter()
|
||||
.any(|event| matches!(&event, PointerEvent::Released(Some(click)) if click.button == button && click.is_triple()))
|
||||
self.pointer_events.iter().any(|event| {
|
||||
matches!(
|
||||
&event,
|
||||
PointerEvent::Released {
|
||||
click: Some(click),
|
||||
button: b,
|
||||
} if *b == button && click.is_triple()
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Was the primary button clicked this frame?
|
||||
@@ -782,18 +864,6 @@ impl PointerState {
|
||||
self.button_clicked(PointerButton::Secondary)
|
||||
}
|
||||
|
||||
// /// Was this button pressed (`!down -> down`) this frame?
|
||||
// /// This can sometimes return `true` even if `any_down() == false`
|
||||
// /// because a press can be shorted than one frame.
|
||||
// pub fn button_pressed(&self, button: PointerButton) -> bool {
|
||||
// self.pointer_events.iter().any(|event| event.is_press())
|
||||
// }
|
||||
|
||||
// /// Was this button released (`down -> !down`) this frame?
|
||||
// pub fn button_released(&self, button: PointerButton) -> bool {
|
||||
// self.pointer_events.iter().any(|event| event.is_release())
|
||||
// }
|
||||
|
||||
/// Is this button currently down?
|
||||
#[inline(always)]
|
||||
pub fn button_down(&self, button: PointerButton) -> bool {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
use crate::*;
|
||||
|
||||
pub fn font_family_ui(ui: &mut Ui, font_family: &mut FontFamily) {
|
||||
let families = ui.fonts().families();
|
||||
let families = ui.fonts(|f| f.families());
|
||||
ui.horizontal(|ui| {
|
||||
for alternative in families {
|
||||
let text = alternative.to_string();
|
||||
@@ -12,7 +12,7 @@ pub fn font_family_ui(ui: &mut Ui, font_family: &mut FontFamily) {
|
||||
}
|
||||
|
||||
pub fn font_id_ui(ui: &mut Ui, font_id: &mut FontId) {
|
||||
let families = ui.fonts().families();
|
||||
let families = ui.fonts(|f| f.families());
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(Slider::new(&mut font_id.size, 4.0..=40.0).max_decimals(1));
|
||||
for alternative in families {
|
||||
|
||||
@@ -109,7 +109,7 @@ impl LayerId {
|
||||
}
|
||||
|
||||
/// A unique identifier of a specific [`Shape`] in a [`PaintList`].
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ShapeIdx(usize);
|
||||
|
||||
/// A list of [`Shape`]s paired with a clip rectangle.
|
||||
|
||||
@@ -75,7 +75,7 @@ impl Region {
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Layout direction, one of [`LeftToRight`](Direction::LeftToRight), [`RightToLeft`](Direction::RightToLeft), [`TopDown`](Direction::TopDown), [`BottomUp`](Direction::BottomUp).
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub enum Direction {
|
||||
LeftToRight,
|
||||
@@ -114,7 +114,7 @@ impl Direction {
|
||||
/// });
|
||||
/// # });
|
||||
/// ```
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
// #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct Layout {
|
||||
/// Main axis direction
|
||||
@@ -229,22 +229,10 @@ impl Layout {
|
||||
}
|
||||
}
|
||||
|
||||
/// For when you want to add a single widget to a layout, and that widget
|
||||
/// should be centered horizontally and vertically.
|
||||
#[inline(always)]
|
||||
pub fn centered(main_dir: Direction) -> Self {
|
||||
Self {
|
||||
main_dir,
|
||||
main_wrap: false,
|
||||
main_align: Align::Center,
|
||||
main_justify: false,
|
||||
cross_align: Align::Center,
|
||||
cross_justify: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// For when you want to add a single widget to a layout, and that widget
|
||||
/// should use up all available space.
|
||||
///
|
||||
/// Only one widget may be added to the inner `Ui`!
|
||||
#[inline(always)]
|
||||
pub fn centered_and_justified(main_dir: Direction) -> Self {
|
||||
Self {
|
||||
|
||||
@@ -324,18 +324,23 @@ pub mod util;
|
||||
pub mod widget_text;
|
||||
pub mod widgets;
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub use accesskit;
|
||||
|
||||
pub use epaint;
|
||||
pub use epaint::ecolor;
|
||||
pub use epaint::emath;
|
||||
|
||||
pub use emath::{lerp, pos2, remap, remap_clamp, vec2, Align, Align2, NumExt, Pos2, Rect, Vec2};
|
||||
#[cfg(feature = "color-hex")]
|
||||
pub use epaint::hex_color;
|
||||
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 epaint::{
|
||||
color, mutex,
|
||||
mutex,
|
||||
text::{FontData, FontDefinitions, FontFamily, FontId, FontTweak},
|
||||
textures::{TextureFilter, TextureOptions, TexturesDelta},
|
||||
ClippedPrimitive, Color32, ColorImage, FontImage, ImageData, Mesh, PaintCallback,
|
||||
PaintCallbackInfo, Rgba, Rounding, Shape, Stroke, TextureHandle, TextureId,
|
||||
ClippedPrimitive, ColorImage, FontImage, ImageData, Mesh, PaintCallback, PaintCallbackInfo,
|
||||
Rounding, Shape, Stroke, TextureHandle, TextureId,
|
||||
};
|
||||
|
||||
pub mod text {
|
||||
@@ -358,11 +363,11 @@ pub use {
|
||||
input_state::{InputState, MultiTouchInfo, PointerState},
|
||||
layers::{LayerId, Order},
|
||||
layout::*,
|
||||
memory::Memory,
|
||||
memory::{Memory, Options},
|
||||
painter::Painter,
|
||||
response::{InnerResponse, Response},
|
||||
sense::Sense,
|
||||
style::{FontSelection, Style, TextStyle, Visuals},
|
||||
style::{FontSelection, Margin, Style, TextStyle, Visuals},
|
||||
text::{Galley, TextFormat},
|
||||
ui::Ui,
|
||||
widget_text::{RichText, WidgetText},
|
||||
@@ -504,7 +509,7 @@ pub mod special_emojis {
|
||||
}
|
||||
|
||||
/// The different types of built-in widgets in egui
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub enum WidgetType {
|
||||
Label, // TODO(emilk): emit Label events
|
||||
@@ -549,3 +554,30 @@ pub fn __run_test_ui(mut add_contents: impl FnMut(&mut Ui)) {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub fn accesskit_root_id() -> Id {
|
||||
Id::new("accesskit_root")
|
||||
}
|
||||
|
||||
/// Return a tree update that the egui integration should provide to the
|
||||
/// AccessKit adapter if it cannot immediately run the egui application
|
||||
/// to get a full tree update after running [`Context::enable_accesskit`].
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub fn accesskit_placeholder_tree_update() -> accesskit::TreeUpdate {
|
||||
use accesskit::{Node, Role, Tree, TreeUpdate};
|
||||
use std::sync::Arc;
|
||||
|
||||
let root_id = accesskit_root_id().accesskit_id();
|
||||
TreeUpdate {
|
||||
nodes: vec![(
|
||||
root_id,
|
||||
Arc::new(Node {
|
||||
role: Role::Window,
|
||||
..Default::default()
|
||||
}),
|
||||
)],
|
||||
tree: Some(Tree::new(root_id)),
|
||||
focus: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,9 +52,10 @@ pub struct Memory {
|
||||
/// type CharCountCache<'a> = FrameCache<usize, CharCounter>;
|
||||
///
|
||||
/// # let mut ctx = egui::Context::default();
|
||||
/// let mut memory = ctx.memory();
|
||||
/// let cache = memory.caches.cache::<CharCountCache<'_>>();
|
||||
/// assert_eq!(cache.get("hello"), 5);
|
||||
/// ctx.memory_mut(|mem| {
|
||||
/// let cache = mem.caches.cache::<CharCountCache<'_>>();
|
||||
/// assert_eq!(cache.get("hello"), 5);
|
||||
/// });
|
||||
/// ```
|
||||
#[cfg_attr(feature = "persistence", serde(skip))]
|
||||
pub caches: crate::util::cache::CacheStorage,
|
||||
@@ -102,9 +103,15 @@ pub struct Options {
|
||||
/// Controls the tessellator.
|
||||
pub tessellation_options: epaint::TessellationOptions,
|
||||
|
||||
/// This does not at all change the behavior of egui,
|
||||
/// but is a signal to any backend that we want the [`crate::PlatformOutput::events`] read out loud.
|
||||
/// This is a signal to any backend that we want the [`crate::PlatformOutput::events`] read out loud.
|
||||
///
|
||||
/// The only change to egui is that labels can be focused by pressing tab.
|
||||
///
|
||||
/// Screen readers is an experimental feature of egui, and not supported on all platforms.
|
||||
///
|
||||
/// `eframe` supports it only on web, using the `web_screen_reader` feature flag,
|
||||
/// but you should consider using [AccessKit](https://github.com/AccessKit/accesskit) instead,
|
||||
/// which `eframe` supports.
|
||||
pub screen_reader: bool,
|
||||
|
||||
/// If true, the most common glyphs (ASCII) are pre-rendered to the texture atlas.
|
||||
@@ -166,7 +173,7 @@ pub(crate) struct Interaction {
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(crate) struct Focus {
|
||||
/// The widget with keyboard focus (i.e. a text input field).
|
||||
id: Option<Id>,
|
||||
pub(crate) id: Option<Id>,
|
||||
|
||||
/// What had keyboard focus previous frame?
|
||||
id_previous_frame: Option<Id>,
|
||||
@@ -174,6 +181,9 @@ pub(crate) struct Focus {
|
||||
/// Give focus to this widget next frame
|
||||
id_next_frame: Option<Id>,
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
id_requested_by_accesskit: Option<accesskit::NodeId>,
|
||||
|
||||
/// If set, the next widget that is interested in focus will automatically get it.
|
||||
/// Probably because the user pressed Tab.
|
||||
give_to_next: bool,
|
||||
@@ -231,6 +241,11 @@ impl Focus {
|
||||
self.id = Some(id);
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
{
|
||||
self.id_requested_by_accesskit = None;
|
||||
}
|
||||
|
||||
self.pressed_tab = false;
|
||||
self.pressed_shift_tab = false;
|
||||
for event in &new_input.events {
|
||||
@@ -240,6 +255,7 @@ impl Focus {
|
||||
key: crate::Key::Escape,
|
||||
pressed: true,
|
||||
modifiers: _,
|
||||
..
|
||||
}
|
||||
) {
|
||||
self.id = None;
|
||||
@@ -251,6 +267,7 @@ impl Focus {
|
||||
key: crate::Key::Tab,
|
||||
pressed: true,
|
||||
modifiers,
|
||||
..
|
||||
} = event
|
||||
{
|
||||
if !self.is_focus_locked {
|
||||
@@ -261,6 +278,18 @@ impl Focus {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
{
|
||||
if let crate::Event::AccessKitActionRequest(accesskit::ActionRequest {
|
||||
action: accesskit::Action::Focus,
|
||||
target,
|
||||
data: None,
|
||||
}) = event
|
||||
{
|
||||
self.id_requested_by_accesskit = Some(*target);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,6 +310,17 @@ impl Focus {
|
||||
}
|
||||
|
||||
fn interested_in_focus(&mut self, id: Id) {
|
||||
#[cfg(feature = "accesskit")]
|
||||
{
|
||||
if self.id_requested_by_accesskit == Some(id.accesskit_id()) {
|
||||
self.id = Some(id);
|
||||
self.id_requested_by_accesskit = None;
|
||||
self.give_to_next = false;
|
||||
self.pressed_tab = false;
|
||||
self.pressed_shift_tab = false;
|
||||
}
|
||||
}
|
||||
|
||||
if self.give_to_next && !self.had_focus_last_frame(id) {
|
||||
self.id = Some(id);
|
||||
self.give_to_next = false;
|
||||
@@ -293,7 +333,7 @@ impl Focus {
|
||||
self.id_next_frame = self.last_interested; // frame-delay so gained_focus works
|
||||
self.pressed_shift_tab = false;
|
||||
}
|
||||
} else if self.pressed_tab && self.id == None && !self.give_to_next {
|
||||
} else if self.pressed_tab && self.id.is_none() && !self.give_to_next {
|
||||
// nothing has focus and the user pressed tab - give focus to the first widgets that wants it:
|
||||
self.id = Some(id);
|
||||
self.pressed_tab = false;
|
||||
@@ -372,7 +412,7 @@ impl Memory {
|
||||
}
|
||||
|
||||
/// Is the keyboard focus locked on this widget? If so the focus won't move even if the user presses the tab key.
|
||||
pub fn has_lock_focus(&mut self, id: Id) -> bool {
|
||||
pub fn has_lock_focus(&self, id: Id) -> bool {
|
||||
if self.had_focus_last_frame(id) && self.has_focus(id) {
|
||||
self.interaction.focus.is_focus_locked
|
||||
} else {
|
||||
@@ -400,8 +440,13 @@ impl Memory {
|
||||
|
||||
/// Register this widget as being interested in getting keyboard focus.
|
||||
/// This will allow the user to select it with tab and shift-tab.
|
||||
/// This is normally done automatically when handling interactions,
|
||||
/// but it is sometimes useful to pre-register interest in focus,
|
||||
/// e.g. before deciding which type of underlying widget to use,
|
||||
/// as in the [`crate::DragValue`] widget, so a widget can be focused
|
||||
/// and rendered correctly in a single frame.
|
||||
#[inline(always)]
|
||||
pub(crate) fn interested_in_focus(&mut self, id: Id) {
|
||||
pub fn interested_in_focus(&mut self, id: Id) {
|
||||
self.interaction.focus.interested_in_focus(id);
|
||||
}
|
||||
|
||||
@@ -441,6 +486,10 @@ impl Memory {
|
||||
self.popup == Some(popup_id) || self.everything_is_visible()
|
||||
}
|
||||
|
||||
pub fn any_popup_open(&self) -> bool {
|
||||
self.popup.is_some() || self.everything_is_visible()
|
||||
}
|
||||
|
||||
pub fn open_popup(&mut self, popup_id: Id) {
|
||||
self.popup = Some(popup_id);
|
||||
}
|
||||
@@ -551,8 +600,8 @@ impl Areas {
|
||||
pub fn visible_layer_ids(&self) -> ahash::HashSet<LayerId> {
|
||||
self.visible_last_frame
|
||||
.iter()
|
||||
.cloned()
|
||||
.chain(self.visible_current_frame.iter().cloned())
|
||||
.copied()
|
||||
.chain(self.visible_current_frame.iter().copied())
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
||||
@@ -31,11 +31,11 @@ pub(crate) struct BarState {
|
||||
|
||||
impl BarState {
|
||||
fn load(ctx: &Context, bar_id: Id) -> Self {
|
||||
ctx.data().get_temp::<Self>(bar_id).unwrap_or_default()
|
||||
ctx.data_mut(|d| d.get_temp::<Self>(bar_id).unwrap_or_default())
|
||||
}
|
||||
|
||||
fn store(self, ctx: &Context, bar_id: Id) {
|
||||
ctx.data().insert_temp(bar_id, self);
|
||||
ctx.data_mut(|d| d.insert_temp(bar_id, self));
|
||||
}
|
||||
|
||||
/// Show a menu at pointer if primary-clicked response.
|
||||
@@ -66,10 +66,10 @@ impl std::ops::DerefMut for BarState {
|
||||
|
||||
fn set_menu_style(style: &mut Style) {
|
||||
style.spacing.button_padding = vec2(2.0, 0.0);
|
||||
style.visuals.widgets.active.bg_stroke = Stroke::none();
|
||||
style.visuals.widgets.hovered.bg_stroke = Stroke::none();
|
||||
style.visuals.widgets.inactive.bg_fill = Color32::TRANSPARENT;
|
||||
style.visuals.widgets.inactive.bg_stroke = Stroke::none();
|
||||
style.visuals.widgets.active.bg_stroke = Stroke::NONE;
|
||||
style.visuals.widgets.hovered.bg_stroke = Stroke::NONE;
|
||||
style.visuals.widgets.inactive.weak_bg_fill = Color32::TRANSPARENT;
|
||||
style.visuals.widgets.inactive.bg_stroke = Stroke::NONE;
|
||||
}
|
||||
|
||||
/// The menu bar goes well in a [`TopBottomPanel::top`],
|
||||
@@ -100,6 +100,20 @@ pub fn menu_button<R>(
|
||||
stationary_menu_impl(ui, title, Box::new(add_contents))
|
||||
}
|
||||
|
||||
/// Construct a top level menu with an image in a menu bar. This would be e.g. "File", "Edit" etc.
|
||||
///
|
||||
/// Responds to primary clicks.
|
||||
///
|
||||
/// Returns `None` if the menu is not open.
|
||||
pub fn menu_image_button<R>(
|
||||
ui: &mut Ui,
|
||||
texture_id: TextureId,
|
||||
image_size: impl Into<Vec2>,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<Option<R>> {
|
||||
stationary_menu_image_impl(ui, texture_id, image_size, Box::new(add_contents))
|
||||
}
|
||||
|
||||
/// Construct a nested sub menu in another menu.
|
||||
///
|
||||
/// Opens on hover.
|
||||
@@ -129,9 +143,10 @@ pub(crate) fn menu_ui<'c, R>(
|
||||
|
||||
let area = Area::new(menu_id)
|
||||
.order(Order::Foreground)
|
||||
.constrain(true)
|
||||
.fixed_pos(pos)
|
||||
.interactable(true)
|
||||
.drag_bounds(Rect::EVERYTHING);
|
||||
.drag_bounds(ctx.screen_rect());
|
||||
let inner_response = area.show(ctx, |ui| {
|
||||
set_menu_style(ui.style_mut());
|
||||
|
||||
@@ -166,7 +181,7 @@ fn stationary_menu_impl<'c, R>(
|
||||
let mut button = Button::new(title);
|
||||
|
||||
if bar_state.open_menu.is_menu_open(menu_id) {
|
||||
button = button.fill(ui.visuals().widgets.open.bg_fill);
|
||||
button = button.fill(ui.visuals().widgets.open.weak_bg_fill);
|
||||
button = button.stroke(ui.visuals().widgets.open.bg_stroke);
|
||||
}
|
||||
|
||||
@@ -177,6 +192,25 @@ fn stationary_menu_impl<'c, R>(
|
||||
InnerResponse::new(inner.map(|r| r.inner), button_response)
|
||||
}
|
||||
|
||||
/// Build a top level menu with an image button.
|
||||
///
|
||||
/// Responds to primary clicks.
|
||||
fn stationary_menu_image_impl<'c, R>(
|
||||
ui: &mut Ui,
|
||||
texture_id: TextureId,
|
||||
image_size: impl Into<Vec2>,
|
||||
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
|
||||
) -> InnerResponse<Option<R>> {
|
||||
let bar_id = ui.id();
|
||||
|
||||
let mut bar_state = BarState::load(ui.ctx(), bar_id);
|
||||
let button_response = ui.add(ImageButton::new(texture_id, image_size));
|
||||
let inner = bar_state.bar_menu(&button_response, add_contents);
|
||||
|
||||
bar_state.store(ui.ctx(), bar_id);
|
||||
InnerResponse::new(inner.map(|r| r.inner), button_response)
|
||||
}
|
||||
|
||||
/// Response to secondary clicks (right-clicks) by showing the given menu.
|
||||
pub(crate) fn context_menu(
|
||||
response: &Response,
|
||||
@@ -277,10 +311,9 @@ impl MenuRoot {
|
||||
root: &mut MenuRootManager,
|
||||
id: Id,
|
||||
) -> MenuResponse {
|
||||
// Lock the input once for the whole function call (see https://github.com/emilk/egui/pull/1380).
|
||||
let input = response.ctx.input();
|
||||
|
||||
if (response.clicked() && root.is_menu_open(id)) || input.key_pressed(Key::Escape) {
|
||||
if (response.clicked() && root.is_menu_open(id))
|
||||
|| response.ctx.input(|i| i.key_pressed(Key::Escape))
|
||||
{
|
||||
// menu open and button clicked or esc pressed
|
||||
return MenuResponse::Close;
|
||||
} else if (response.clicked() && !root.is_menu_open(id))
|
||||
@@ -288,12 +321,10 @@ impl MenuRoot {
|
||||
{
|
||||
// menu not open and button clicked
|
||||
// or button hovered while other menu is open
|
||||
drop(input);
|
||||
|
||||
let mut pos = response.rect.left_bottom();
|
||||
if let Some(root) = root.inner.as_mut() {
|
||||
let menu_rect = root.menu_state.read().rect;
|
||||
let screen_rect = response.ctx.input().screen_rect;
|
||||
let screen_rect = response.ctx.input(|i| i.screen_rect);
|
||||
|
||||
if pos.y + menu_rect.height() > screen_rect.max.y {
|
||||
pos.y = screen_rect.max.y - menu_rect.height() - response.rect.height();
|
||||
@@ -305,8 +336,11 @@ impl MenuRoot {
|
||||
}
|
||||
|
||||
return MenuResponse::Create(pos, id);
|
||||
} else if input.pointer.any_pressed() && input.pointer.primary_down() {
|
||||
if let Some(pos) = input.pointer.interact_pos() {
|
||||
} else if response
|
||||
.ctx
|
||||
.input(|i| i.pointer.any_pressed() && i.pointer.primary_down())
|
||||
{
|
||||
if let Some(pos) = response.ctx.input(|i| i.pointer.interact_pos()) {
|
||||
if let Some(root) = root.inner.as_mut() {
|
||||
if root.id == id {
|
||||
// pressed somewhere while this menu is open
|
||||
@@ -329,26 +363,28 @@ impl MenuRoot {
|
||||
id: Id,
|
||||
) -> MenuResponse {
|
||||
let response = response.interact(Sense::click());
|
||||
let pointer = &response.ctx.input().pointer;
|
||||
if pointer.any_pressed() {
|
||||
if let Some(pos) = pointer.interact_pos() {
|
||||
let mut destroy = false;
|
||||
let mut in_old_menu = false;
|
||||
if let Some(root) = root {
|
||||
let menu_state = root.menu_state.read();
|
||||
in_old_menu = menu_state.area_contains(pos);
|
||||
destroy = root.id == response.id;
|
||||
}
|
||||
if !in_old_menu {
|
||||
if response.hovered() && pointer.secondary_down() {
|
||||
return MenuResponse::Create(pos, id);
|
||||
} else if (response.hovered() && pointer.primary_down()) || destroy {
|
||||
return MenuResponse::Close;
|
||||
response.ctx.input(|input| {
|
||||
let pointer = &input.pointer;
|
||||
if pointer.any_pressed() {
|
||||
if let Some(pos) = pointer.interact_pos() {
|
||||
let mut destroy = false;
|
||||
let mut in_old_menu = false;
|
||||
if let Some(root) = root {
|
||||
let menu_state = root.menu_state.read();
|
||||
in_old_menu = menu_state.area_contains(pos);
|
||||
destroy = root.id == response.id;
|
||||
}
|
||||
if !in_old_menu {
|
||||
if response.hovered() && pointer.secondary_down() {
|
||||
return MenuResponse::Create(pos, id);
|
||||
} else if (response.hovered() && pointer.primary_down()) || destroy {
|
||||
return MenuResponse::Close;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
MenuResponse::Stay
|
||||
MenuResponse::Stay
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_menu_response(root: &mut MenuRootManager, menu_response: MenuResponse) {
|
||||
@@ -410,7 +446,7 @@ impl SubMenuButton {
|
||||
sub_id: Id,
|
||||
) -> &'a WidgetVisuals {
|
||||
if menu_state.is_open(sub_id) {
|
||||
&ui.style().visuals.widgets.hovered
|
||||
&ui.style().visuals.widgets.open
|
||||
} else {
|
||||
ui.style().interact(response)
|
||||
}
|
||||
@@ -439,11 +475,12 @@ impl SubMenuButton {
|
||||
text_galley.size().x + icon_galley.size().x,
|
||||
text_galley.size().y.max(icon_galley.size().y),
|
||||
);
|
||||
let desired_size = text_and_icon_size + 2.0 * button_padding;
|
||||
let mut desired_size = text_and_icon_size + 2.0 * button_padding;
|
||||
desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y);
|
||||
|
||||
let (rect, response) = ui.allocate_at_least(desired_size, sense);
|
||||
response.widget_info(|| {
|
||||
crate::WidgetInfo::labeled(crate::WidgetType::Button, &text_galley.text())
|
||||
crate::WidgetInfo::labeled(crate::WidgetType::Button, text_galley.text())
|
||||
});
|
||||
|
||||
if ui.is_rect_visible(rect) {
|
||||
@@ -459,7 +496,7 @@ impl SubMenuButton {
|
||||
ui.painter().rect_filled(
|
||||
rect.expand(visuals.expansion),
|
||||
visuals.rounding,
|
||||
visuals.bg_fill,
|
||||
visuals.weak_bg_fill,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -491,7 +528,7 @@ impl SubMenu {
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<Option<R>> {
|
||||
let sub_id = ui.id().with(self.button.index);
|
||||
let button = self.button.show(ui, &*self.parent_state.read(), sub_id);
|
||||
let button = self.button.show(ui, &self.parent_state.read(), sub_id);
|
||||
self.parent_state
|
||||
.write()
|
||||
.submenu_button_interaction(ui, sub_id, &button);
|
||||
@@ -571,15 +608,15 @@ impl MenuState {
|
||||
|
||||
/// Sense button interaction opening and closing submenu.
|
||||
fn submenu_button_interaction(&mut self, ui: &mut Ui, sub_id: Id, button: &Response) {
|
||||
let pointer = &ui.input().pointer.clone();
|
||||
let pointer = ui.input(|i| i.pointer.clone());
|
||||
let open = self.is_open(sub_id);
|
||||
if self.moving_towards_current_submenu(pointer) {
|
||||
if self.moving_towards_current_submenu(&pointer) {
|
||||
// ensure to repaint once even when pointer is not moving
|
||||
ui.ctx().request_repaint();
|
||||
} else if !open && button.hovered() {
|
||||
let pos = button.rect.right_top();
|
||||
self.open_submenu(sub_id, pos);
|
||||
} else if open && !button.hovered() && !self.hovering_current_submenu(pointer) {
|
||||
} else if open && !button.hovered() && !self.hovering_current_submenu(&pointer) {
|
||||
self.close_submenu();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ use crate::{
|
||||
Color32, Context, FontId,
|
||||
};
|
||||
use epaint::{
|
||||
mutex::{RwLockReadGuard, RwLockWriteGuard},
|
||||
text::{Fonts, Galley},
|
||||
CircleShape, RectShape, Rounding, Shape, Stroke,
|
||||
};
|
||||
@@ -105,10 +104,12 @@ impl Painter {
|
||||
&self.ctx
|
||||
}
|
||||
|
||||
/// Available fonts.
|
||||
/// Read-only access to the shared [`Fonts`].
|
||||
///
|
||||
/// See [`Context`] documentation for how locks work.
|
||||
#[inline(always)]
|
||||
pub fn fonts(&self) -> RwLockReadGuard<'_, Fonts> {
|
||||
self.ctx.fonts()
|
||||
pub fn fonts<R>(&self, reader: impl FnOnce(&Fonts) -> R) -> R {
|
||||
self.ctx.fonts(reader)
|
||||
}
|
||||
|
||||
/// Where we paint
|
||||
@@ -152,8 +153,9 @@ impl Painter {
|
||||
|
||||
/// ## Low level
|
||||
impl Painter {
|
||||
fn paint_list(&self) -> RwLockWriteGuard<'_, PaintList> {
|
||||
RwLockWriteGuard::map(self.ctx.graphics(), |g| g.list(self.layer_id))
|
||||
#[inline]
|
||||
fn paint_list<R>(&self, writer: impl FnOnce(&mut PaintList) -> R) -> R {
|
||||
self.ctx.graphics_mut(|g| writer(g.list(self.layer_id)))
|
||||
}
|
||||
|
||||
fn transform_shape(&self, shape: &mut Shape) {
|
||||
@@ -167,11 +169,11 @@ impl Painter {
|
||||
/// NOTE: all coordinates are screen coordinates!
|
||||
pub fn add(&self, shape: impl Into<Shape>) -> ShapeIdx {
|
||||
if self.fade_to_color == Some(Color32::TRANSPARENT) {
|
||||
self.paint_list().add(self.clip_rect, Shape::Noop)
|
||||
self.paint_list(|l| l.add(self.clip_rect, Shape::Noop))
|
||||
} else {
|
||||
let mut shape = shape.into();
|
||||
self.transform_shape(&mut shape);
|
||||
self.paint_list().add(self.clip_rect, shape)
|
||||
self.paint_list(|l| l.add(self.clip_rect, shape))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,9 +189,9 @@ impl Painter {
|
||||
self.transform_shape(&mut shape);
|
||||
shape
|
||||
});
|
||||
self.paint_list().extend(self.clip_rect, shapes);
|
||||
self.paint_list(|l| l.extend(self.clip_rect, shapes));
|
||||
} else {
|
||||
self.paint_list().extend(self.clip_rect, shapes);
|
||||
self.paint_list(|l| l.extend(self.clip_rect, shapes));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -200,7 +202,7 @@ impl Painter {
|
||||
}
|
||||
let mut shape = shape.into();
|
||||
self.transform_shape(&mut shape);
|
||||
self.paint_list().set(idx, self.clip_rect, shape);
|
||||
self.paint_list(|l| l.set(idx, self.clip_rect, shape));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +210,12 @@ impl Painter {
|
||||
impl Painter {
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn debug_rect(&self, rect: Rect, color: Color32, text: impl ToString) {
|
||||
self.rect_stroke(rect, 0.0, (1.0, color));
|
||||
self.rect(
|
||||
rect,
|
||||
0.0,
|
||||
color.additive().linear_multiply(0.015),
|
||||
(1.0, color),
|
||||
);
|
||||
self.text(
|
||||
rect.min,
|
||||
Align2::LEFT_TOP,
|
||||
@@ -238,7 +245,7 @@ impl Painter {
|
||||
self.add(Shape::rect_filled(
|
||||
frame_rect,
|
||||
0.0,
|
||||
Color32::from_black_alpha(240),
|
||||
Color32::from_black_alpha(150),
|
||||
));
|
||||
self.galley(rect.min, galley);
|
||||
frame_rect
|
||||
@@ -400,7 +407,7 @@ impl Painter {
|
||||
color: crate::Color32,
|
||||
wrap_width: f32,
|
||||
) -> Arc<Galley> {
|
||||
self.fonts().layout(text, font_id, color, wrap_width)
|
||||
self.fonts(|f| f.layout(text, font_id, color, wrap_width))
|
||||
}
|
||||
|
||||
/// Will line break at `\n`.
|
||||
@@ -413,7 +420,7 @@ impl Painter {
|
||||
font_id: FontId,
|
||||
color: crate::Color32,
|
||||
) -> Arc<Galley> {
|
||||
self.fonts().layout(text, font_id, color, f32::INFINITY)
|
||||
self.fonts(|f| f.layout(text, font_id, color, f32::INFINITY))
|
||||
}
|
||||
|
||||
/// Paint text that has already been layed out in a [`Galley`].
|
||||
@@ -443,6 +450,6 @@ impl Painter {
|
||||
|
||||
fn tint_shape_towards(shape: &mut Shape, target: Color32) {
|
||||
epaint::shape_transform::adjust_colors(shape, &|color| {
|
||||
*color = crate::color::tint_color_towards(*color, target);
|
||||
*color = crate::ecolor::tint_color_towards(*color, target);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ use crate::{
|
||||
///
|
||||
/// Whenever something gets added to a [`Ui`], a [`Response`] object is returned.
|
||||
/// [`ui.add`] returns a [`Response`], as does [`ui.button`], and all similar shortcuts.
|
||||
// TODO(emilk): we should be using bit sets instead of so many bools
|
||||
#[derive(Clone)]
|
||||
pub struct Response {
|
||||
// CONTEXT:
|
||||
@@ -42,6 +43,10 @@ pub struct Response {
|
||||
#[doc(hidden)]
|
||||
pub hovered: bool,
|
||||
|
||||
/// The widget is highlighted via a call to [`Self::highlight`] or [`Context::highlight_widget`].
|
||||
#[doc(hidden)]
|
||||
pub highlighted: bool,
|
||||
|
||||
/// The pointer clicked this thing this frame.
|
||||
#[doc(hidden)]
|
||||
pub clicked: [bool; NUM_POINTER_BUTTONS],
|
||||
@@ -52,7 +57,7 @@ pub struct Response {
|
||||
pub double_clicked: [bool; NUM_POINTER_BUTTONS],
|
||||
|
||||
/// The thing was triple-clicked.
|
||||
pub(crate) triple_clicked: [bool; NUM_POINTER_BUTTONS],
|
||||
pub triple_clicked: [bool; NUM_POINTER_BUTTONS],
|
||||
|
||||
/// The widgets is being dragged
|
||||
#[doc(hidden)]
|
||||
@@ -90,6 +95,7 @@ impl std::fmt::Debug for Response {
|
||||
sense,
|
||||
enabled,
|
||||
hovered,
|
||||
highlighted,
|
||||
clicked,
|
||||
double_clicked,
|
||||
triple_clicked,
|
||||
@@ -106,6 +112,7 @@ impl std::fmt::Debug for Response {
|
||||
.field("sense", sense)
|
||||
.field("enabled", enabled)
|
||||
.field("hovered", hovered)
|
||||
.field("highlighted", highlighted)
|
||||
.field("clicked", clicked)
|
||||
.field("double_clicked", double_clicked)
|
||||
.field("triple_clicked", triple_clicked)
|
||||
@@ -173,23 +180,25 @@ impl Response {
|
||||
// We do not use self.clicked(), because we want to catch all clicks within our frame,
|
||||
// even if we aren't clickable (or even enabled).
|
||||
// This is important for windows and such that should close then the user clicks elsewhere.
|
||||
let pointer = &self.ctx.input().pointer;
|
||||
self.ctx.input(|i| {
|
||||
let pointer = &i.pointer;
|
||||
|
||||
if pointer.any_click() {
|
||||
// We detect clicks/hover on a "interact_rect" that is slightly larger than
|
||||
// self.rect. See Context::interact.
|
||||
// This means we can be hovered and clicked even though `!self.rect.contains(pos)` is true,
|
||||
// hence the extra complexity here.
|
||||
if self.hovered() {
|
||||
false
|
||||
} else if let Some(pos) = pointer.interact_pos() {
|
||||
!self.rect.contains(pos)
|
||||
if pointer.any_click() {
|
||||
// We detect clicks/hover on a "interact_rect" that is slightly larger than
|
||||
// self.rect. See Context::interact.
|
||||
// This means we can be hovered and clicked even though `!self.rect.contains(pos)` is true,
|
||||
// hence the extra complexity here.
|
||||
if self.hovered() {
|
||||
false
|
||||
} else if let Some(pos) = pointer.interact_pos() {
|
||||
!self.rect.contains(pos)
|
||||
} else {
|
||||
false // clicked without a pointer, weird
|
||||
}
|
||||
} else {
|
||||
false // clicked without a pointer, weird
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Was the widget enabled?
|
||||
@@ -211,20 +220,24 @@ impl Response {
|
||||
self.hovered
|
||||
}
|
||||
|
||||
/// The widget is highlighted via a call to [`Self::highlight`] or [`Context::highlight_widget`].
|
||||
#[doc(hidden)]
|
||||
pub fn highlighted(&self) -> bool {
|
||||
self.highlighted
|
||||
}
|
||||
|
||||
/// This widget has the keyboard focus (i.e. is receiving key presses).
|
||||
///
|
||||
/// This function only returns true if the UI as a whole (e.g. window)
|
||||
/// also has the keyboard focus. That makes this function suitable
|
||||
/// for style choices, e.g. a thicker border around focused widgets.
|
||||
pub fn has_focus(&self) -> bool {
|
||||
// Access input and memory in separate statements to prevent deadlock.
|
||||
let has_global_focus = self.ctx.input().raw.has_focus;
|
||||
has_global_focus && self.ctx.memory().has_focus(self.id)
|
||||
self.ctx.input(|i| i.raw.has_focus) && self.ctx.memory(|mem| mem.has_focus(self.id))
|
||||
}
|
||||
|
||||
/// True if this widget has keyboard focus this frame, but didn't last frame.
|
||||
pub fn gained_focus(&self) -> bool {
|
||||
self.ctx.memory().gained_focus(self.id)
|
||||
self.ctx.memory(|mem| mem.gained_focus(self.id))
|
||||
}
|
||||
|
||||
/// The widget had keyboard focus and lost it,
|
||||
@@ -236,29 +249,29 @@ impl Response {
|
||||
/// # let mut my_text = String::new();
|
||||
/// # fn do_request(_: &str) {}
|
||||
/// let response = ui.text_edit_singleline(&mut my_text);
|
||||
/// if response.lost_focus() && ui.input().key_pressed(egui::Key::Enter) {
|
||||
/// if response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
||||
/// do_request(&my_text);
|
||||
/// }
|
||||
/// # });
|
||||
/// ```
|
||||
pub fn lost_focus(&self) -> bool {
|
||||
self.ctx.memory().lost_focus(self.id)
|
||||
self.ctx.memory(|mem| mem.lost_focus(self.id))
|
||||
}
|
||||
|
||||
/// Request that this widget get keyboard focus.
|
||||
pub fn request_focus(&self) {
|
||||
self.ctx.memory().request_focus(self.id);
|
||||
self.ctx.memory_mut(|mem| mem.request_focus(self.id));
|
||||
}
|
||||
|
||||
/// Surrender keyboard focus for this widget.
|
||||
pub fn surrender_focus(&self) {
|
||||
self.ctx.memory().surrender_focus(self.id);
|
||||
self.ctx.memory_mut(|mem| mem.surrender_focus(self.id));
|
||||
}
|
||||
|
||||
/// The widgets is being dragged.
|
||||
///
|
||||
/// To find out which button(s), query [`crate::PointerState::button_down`]
|
||||
/// (`ui.input().pointer.button_down(…)`).
|
||||
/// (`ui.input(|i| i.pointer.button_down(…))`).
|
||||
///
|
||||
/// Note that the widget must be sensing drags with [`Sense::drag`].
|
||||
/// [`crate::DragValue`] senses drags; [`crate::Label`] does not (unless you call [`crate::Label::sense`]).
|
||||
@@ -270,12 +283,17 @@ impl Response {
|
||||
}
|
||||
|
||||
pub fn dragged_by(&self, button: PointerButton) -> bool {
|
||||
self.dragged() && self.ctx.input().pointer.button_down(button)
|
||||
self.dragged() && self.ctx.input(|i| i.pointer.button_down(button))
|
||||
}
|
||||
|
||||
/// Did a drag on this widgets begin this frame?
|
||||
pub fn drag_started(&self) -> bool {
|
||||
self.dragged && self.ctx.input().pointer.any_pressed()
|
||||
self.dragged && self.ctx.input(|i| i.pointer.any_pressed())
|
||||
}
|
||||
|
||||
/// Did a drag on this widgets by the button begin this frame?
|
||||
pub fn drag_started_by(&self, button: PointerButton) -> bool {
|
||||
self.drag_started() && self.ctx.input(|i| i.pointer.button_pressed(button))
|
||||
}
|
||||
|
||||
/// The widget was being dragged, but now it has been released.
|
||||
@@ -283,10 +301,15 @@ impl Response {
|
||||
self.drag_released
|
||||
}
|
||||
|
||||
/// The widget was being dragged by the button, but now it has been released.
|
||||
pub fn drag_released_by(&self, button: PointerButton) -> bool {
|
||||
self.drag_released() && self.ctx.input(|i| i.pointer.button_released(button))
|
||||
}
|
||||
|
||||
/// If dragged, how many points were we dragged and in what direction?
|
||||
pub fn drag_delta(&self) -> Vec2 {
|
||||
if self.dragged() {
|
||||
self.ctx.input().pointer.delta()
|
||||
self.ctx.input(|i| i.pointer.delta())
|
||||
} else {
|
||||
Vec2::ZERO
|
||||
}
|
||||
@@ -302,7 +325,7 @@ impl Response {
|
||||
/// None if the pointer is outside the response area.
|
||||
pub fn hover_pos(&self) -> Option<Pos2> {
|
||||
if self.hovered() {
|
||||
self.ctx.input().pointer.hover_pos()
|
||||
self.ctx.input(|i| i.pointer.hover_pos())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -392,11 +415,11 @@ impl Response {
|
||||
}
|
||||
|
||||
fn should_show_hover_ui(&self) -> bool {
|
||||
if self.ctx.memory().everything_is_visible() {
|
||||
if self.ctx.memory(|mem| mem.everything_is_visible()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if !self.hovered || !self.ctx.input().pointer.has_pointer() {
|
||||
if !self.hovered || !self.ctx.input(|i| i.pointer.has_pointer()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -404,8 +427,7 @@ impl Response {
|
||||
// We only show the tooltip when the mouse pointer is still,
|
||||
// but once shown we keep showing it until the mouse leaves the parent.
|
||||
|
||||
let is_pointer_still = self.ctx.input().pointer.is_still();
|
||||
if !is_pointer_still && !self.is_tooltip_open() {
|
||||
if !self.ctx.input(|i| i.pointer.is_still()) && !self.is_tooltip_open() {
|
||||
// wait for mouse to stop
|
||||
self.ctx.request_repaint();
|
||||
return false;
|
||||
@@ -414,8 +436,9 @@ impl Response {
|
||||
|
||||
// We don't want tooltips of things while we are dragging them,
|
||||
// but we do want tooltips while holding down on an item on a touch screen.
|
||||
if self.ctx.input().pointer.any_down()
|
||||
&& self.ctx.input().pointer.has_moved_too_much_for_a_click
|
||||
if self
|
||||
.ctx
|
||||
.input(|i| i.pointer.any_down() && i.pointer.has_moved_too_much_for_a_click)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -444,6 +467,17 @@ impl Response {
|
||||
})
|
||||
}
|
||||
|
||||
/// Highlight this widget, to make it look like it is hovered, even if it isn't.
|
||||
///
|
||||
/// The highlight takes on frame to take effect if you call this after the widget has been fully rendered.
|
||||
///
|
||||
/// See also [`Context::highlight_widget`].
|
||||
pub fn highlight(mut self) -> Self {
|
||||
self.ctx.highlight_widget(self.id);
|
||||
self.highlighted = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Show this text when hovering if the widget is disabled.
|
||||
pub fn on_disabled_hover_text(self, text: impl Into<WidgetText>) -> Self {
|
||||
self.on_disabled_hover_ui(|ui| {
|
||||
@@ -454,7 +488,15 @@ impl Response {
|
||||
/// When hovered, use this icon for the mouse cursor.
|
||||
pub fn on_hover_cursor(self, cursor: CursorIcon) -> Self {
|
||||
if self.hovered() {
|
||||
self.ctx.output().cursor_icon = cursor;
|
||||
self.ctx.set_cursor_icon(cursor);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// When hovered or dragged, use this icon for the mouse cursor.
|
||||
pub fn on_hover_and_drag_cursor(self, cursor: CursorIcon) -> Self {
|
||||
if self.hovered() || self.dragged() {
|
||||
self.ctx.set_cursor_icon(cursor);
|
||||
}
|
||||
self
|
||||
}
|
||||
@@ -503,8 +545,10 @@ impl Response {
|
||||
/// # });
|
||||
/// ```
|
||||
pub fn scroll_to_me(&self, align: Option<Align>) {
|
||||
self.ctx.frame_state().scroll_target[0] = Some((self.rect.x_range(), align));
|
||||
self.ctx.frame_state().scroll_target[1] = Some((self.rect.y_range(), align));
|
||||
self.ctx.frame_state_mut(|state| {
|
||||
state.scroll_target[0] = Some((self.rect.x_range(), align));
|
||||
state.scroll_target[1] = Some((self.rect.y_range(), align));
|
||||
});
|
||||
}
|
||||
|
||||
/// For accessibility.
|
||||
@@ -526,10 +570,108 @@ impl Response {
|
||||
None
|
||||
};
|
||||
if let Some(event) = event {
|
||||
self.ctx.output().events.push(event);
|
||||
self.output_event(event);
|
||||
} else {
|
||||
#[cfg(feature = "accesskit")]
|
||||
self.ctx.accesskit_node(self.id, |node| {
|
||||
self.fill_accesskit_node_from_widget_info(node, make_info());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn output_event(&self, event: crate::output::OutputEvent) {
|
||||
#[cfg(feature = "accesskit")]
|
||||
self.ctx.accesskit_node(self.id, |node| {
|
||||
self.fill_accesskit_node_from_widget_info(node, event.widget_info().clone());
|
||||
});
|
||||
self.ctx.output_mut(|o| o.events.push(event));
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub(crate) fn fill_accesskit_node_common(&self, node: &mut accesskit::Node) {
|
||||
node.bounds = Some(accesskit::kurbo::Rect {
|
||||
x0: self.rect.min.x.into(),
|
||||
y0: self.rect.min.y.into(),
|
||||
x1: self.rect.max.x.into(),
|
||||
y1: self.rect.max.y.into(),
|
||||
});
|
||||
if self.sense.focusable {
|
||||
node.focusable = true;
|
||||
}
|
||||
if self.sense.click && node.default_action_verb.is_none() {
|
||||
node.default_action_verb = Some(accesskit::DefaultActionVerb::Click);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
fn fill_accesskit_node_from_widget_info(
|
||||
&self,
|
||||
node: &mut accesskit::Node,
|
||||
info: crate::WidgetInfo,
|
||||
) {
|
||||
use crate::WidgetType;
|
||||
use accesskit::{CheckedState, Role};
|
||||
|
||||
self.fill_accesskit_node_common(node);
|
||||
node.role = match info.typ {
|
||||
WidgetType::Label => Role::StaticText,
|
||||
WidgetType::Link => Role::Link,
|
||||
WidgetType::TextEdit => Role::TextField,
|
||||
WidgetType::Button | WidgetType::ImageButton | WidgetType::CollapsingHeader => {
|
||||
Role::Button
|
||||
}
|
||||
WidgetType::Checkbox => Role::CheckBox,
|
||||
WidgetType::RadioButton => Role::RadioButton,
|
||||
WidgetType::SelectableLabel => Role::ToggleButton,
|
||||
WidgetType::ComboBox => Role::PopupButton,
|
||||
WidgetType::Slider => Role::Slider,
|
||||
WidgetType::DragValue => Role::SpinButton,
|
||||
WidgetType::ColorButton => Role::ColorWell,
|
||||
WidgetType::Other => Role::Unknown,
|
||||
};
|
||||
if let Some(label) = info.label {
|
||||
node.name = Some(label.into());
|
||||
}
|
||||
if let Some(value) = info.current_text_value {
|
||||
node.value = Some(value.into());
|
||||
}
|
||||
if let Some(value) = info.value {
|
||||
node.numeric_value = Some(value);
|
||||
}
|
||||
if let Some(selected) = info.selected {
|
||||
node.checked_state = Some(if selected {
|
||||
CheckedState::True
|
||||
} else {
|
||||
CheckedState::False
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Associate a label with a control for accessibility.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
/// # let mut text = "Arthur".to_string();
|
||||
/// ui.horizontal(|ui| {
|
||||
/// let label = ui.label("Your name: ");
|
||||
/// ui.text_edit_singleline(&mut text).labelled_by(label.id);
|
||||
/// });
|
||||
/// # });
|
||||
/// ```
|
||||
pub fn labelled_by(self, id: Id) -> Self {
|
||||
#[cfg(feature = "accesskit")]
|
||||
self.ctx
|
||||
.accesskit_node(self.id, |node| node.labelled_by.push(id.accesskit_id()));
|
||||
#[cfg(not(feature = "accesskit"))]
|
||||
{
|
||||
let _ = id;
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Response to secondary clicks (right-clicks) by showing the given menu.
|
||||
///
|
||||
/// ```
|
||||
@@ -570,6 +712,7 @@ impl Response {
|
||||
sense: self.sense.union(other.sense),
|
||||
enabled: self.enabled || other.enabled,
|
||||
hovered: self.hovered || other.hovered,
|
||||
highlighted: self.highlighted || other.highlighted,
|
||||
clicked: [
|
||||
self.clicked[0] || other.clicked[0],
|
||||
self.clicked[1] || other.clicked[1],
|
||||
@@ -601,6 +744,13 @@ impl Response {
|
||||
}
|
||||
}
|
||||
|
||||
impl Response {
|
||||
/// Returns a response with a modified [`Self::rect`].
|
||||
pub fn with_new_rect(self, rect: Rect) -> Self {
|
||||
Self { rect, ..self }
|
||||
}
|
||||
}
|
||||
|
||||
/// To summarize the response from many widgets you can use this pattern:
|
||||
///
|
||||
/// ```
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
#![allow(clippy::if_same_then_else)]
|
||||
|
||||
use crate::{color::*, emath::*, FontFamily, FontId, Response, RichText, WidgetText};
|
||||
use crate::{ecolor::*, emath::*, FontFamily, FontId, Response, RichText, WidgetText};
|
||||
use epaint::{Rounding, Shadow, Stroke};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
@@ -174,6 +174,9 @@ pub struct Style {
|
||||
/// ```
|
||||
pub text_styles: BTreeMap<TextStyle, FontId>,
|
||||
|
||||
/// The style to use for [`DragValue`] text.
|
||||
pub drag_value_text_style: TextStyle,
|
||||
|
||||
/// If set, labels buttons wtc will use this to determine whether or not
|
||||
/// to wrap the text at the right edge of the [`Ui`] they are in.
|
||||
/// By default this is `None`.
|
||||
@@ -216,6 +219,7 @@ impl Style {
|
||||
pub fn interact_selectable(&self, response: &Response, selected: bool) -> WidgetVisuals {
|
||||
let mut visuals = *self.visuals.widgets.style(response);
|
||||
if selected {
|
||||
visuals.weak_bg_fill = self.visuals.selection.bg_fill;
|
||||
visuals.bg_fill = self.visuals.selection.bg_fill;
|
||||
// visuals.bg_stroke = self.visuals.selection.stroke;
|
||||
visuals.fg_stroke = self.visuals.selection.stroke;
|
||||
@@ -264,8 +268,11 @@ pub struct Spacing {
|
||||
/// Anything clickable should be (at least) this size.
|
||||
pub interact_size: Vec2, // TODO(emilk): rename min_interact_size ?
|
||||
|
||||
/// Default width of a [`Slider`] and [`ComboBox`](crate::ComboBox).
|
||||
pub slider_width: f32, // TODO(emilk): rename big_interact_size ?
|
||||
/// Default width of a [`Slider`].
|
||||
pub slider_width: f32,
|
||||
|
||||
/// Default (minimum) width of a [`ComboBox`](crate::ComboBox).
|
||||
pub combo_width: f32,
|
||||
|
||||
/// Default width of a [`TextEdit`].
|
||||
pub text_edit_width: f32,
|
||||
@@ -292,6 +299,14 @@ pub struct Spacing {
|
||||
pub combo_height: f32,
|
||||
|
||||
pub scroll_bar_width: f32,
|
||||
|
||||
/// Make sure the scroll handle is at least this big
|
||||
pub scroll_handle_min_length: f32,
|
||||
|
||||
/// Margin between contents and scroll bar.
|
||||
pub scroll_bar_inner_margin: f32,
|
||||
/// Margin between scroll bar and the outer container (e.g. right of a vertical scroll bar).
|
||||
pub scroll_bar_outer_margin: f32,
|
||||
}
|
||||
|
||||
impl Spacing {
|
||||
@@ -355,6 +370,10 @@ impl Margin {
|
||||
pub fn right_bottom(&self) -> Vec2 {
|
||||
vec2(self.right, self.bottom)
|
||||
}
|
||||
|
||||
pub fn is_same(&self) -> bool {
|
||||
self.left == self.right && self.left == self.top && self.left == self.bottom
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f32> for Margin {
|
||||
@@ -459,6 +478,13 @@ pub struct Visuals {
|
||||
|
||||
pub window_rounding: Rounding,
|
||||
pub window_shadow: Shadow,
|
||||
pub window_fill: Color32,
|
||||
pub window_stroke: Stroke,
|
||||
|
||||
pub menu_rounding: Rounding,
|
||||
|
||||
/// Panel background color
|
||||
pub panel_fill: Color32,
|
||||
|
||||
pub popup_shadow: Shadow,
|
||||
|
||||
@@ -476,6 +502,13 @@ pub struct Visuals {
|
||||
|
||||
/// Show a background behind collapsing headers.
|
||||
pub collapsing_header_frame: bool,
|
||||
|
||||
/// Draw a vertical lien left of indented region, in e.g. [`crate::CollapsingHeader`].
|
||||
pub indent_has_left_vline: bool,
|
||||
|
||||
/// Wether or not Grids and Tables should be striped by default
|
||||
/// (have alternating rows differently colored).
|
||||
pub striped: bool,
|
||||
}
|
||||
|
||||
impl Visuals {
|
||||
@@ -490,7 +523,7 @@ impl Visuals {
|
||||
}
|
||||
|
||||
pub fn weak_text_color(&self) -> Color32 {
|
||||
crate::color::tint_color_towards(self.text_color(), self.window_fill())
|
||||
self.gray_out(self.text_color())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
@@ -501,12 +534,25 @@ impl Visuals {
|
||||
/// Window background color.
|
||||
#[inline(always)]
|
||||
pub fn window_fill(&self) -> Color32 {
|
||||
self.widgets.noninteractive.bg_fill
|
||||
self.window_fill
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn window_stroke(&self) -> Stroke {
|
||||
self.widgets.noninteractive.bg_stroke
|
||||
self.window_stroke
|
||||
}
|
||||
|
||||
/// When fading out things, we fade the colors towards this.
|
||||
// TODO(emilk): replace with an alpha
|
||||
#[inline(always)]
|
||||
pub fn fade_out_to_color(&self) -> Color32 {
|
||||
self.widgets.noninteractive.weak_bg_fill
|
||||
}
|
||||
|
||||
/// Returned a "grayed out" version of the given color.
|
||||
#[inline(always)]
|
||||
pub fn gray_out(&self, color: Color32) -> Color32 {
|
||||
crate::ecolor::tint_color_towards(color, self.fade_out_to_color())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -533,7 +579,9 @@ pub struct Widgets {
|
||||
/// The style of an interactive widget, such as a button, at rest.
|
||||
pub inactive: WidgetVisuals,
|
||||
|
||||
/// The style of an interactive widget while you hover it.
|
||||
/// The style of an interactive widget while you hover it, or when it is highlighted.
|
||||
///
|
||||
/// See [`Response::hovered`], [`Response::highlighted`] and [`Response::highlight`].
|
||||
pub hovered: WidgetVisuals,
|
||||
|
||||
/// The style of an interactive widget as you are clicking or dragging it.
|
||||
@@ -549,7 +597,7 @@ impl Widgets {
|
||||
&self.noninteractive
|
||||
} else if response.is_pointer_button_down_on() || response.has_focus() {
|
||||
&self.active
|
||||
} else if response.hovered() {
|
||||
} else if response.hovered() || response.highlighted() {
|
||||
&self.hovered
|
||||
} else {
|
||||
&self.inactive
|
||||
@@ -561,9 +609,17 @@ impl Widgets {
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct WidgetVisuals {
|
||||
/// Background color of widget.
|
||||
/// Background color of widgets that must have a background fill,
|
||||
/// such as the slider background, a checkbox background, or a radio button background.
|
||||
///
|
||||
/// Must never be [`Color32::TRANSPARENT`].
|
||||
pub bg_fill: Color32,
|
||||
|
||||
/// Background color of widgets that can _optionally_ have a background fill, such as buttons.
|
||||
///
|
||||
/// May be [`Color32::TRANSPARENT`].
|
||||
pub weak_bg_fill: Color32,
|
||||
|
||||
/// For surrounding rectangle of things that need it,
|
||||
/// like buttons, the box of the checkbox, etc.
|
||||
/// Should maybe be called `frame_stroke`.
|
||||
@@ -587,7 +643,7 @@ impl WidgetVisuals {
|
||||
}
|
||||
|
||||
/// Options for help debug egui by adding extra visualization
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct DebugOptions {
|
||||
/// However over widgets to see their rectangles
|
||||
@@ -630,6 +686,7 @@ impl Default for Style {
|
||||
override_font_id: None,
|
||||
override_text_style: None,
|
||||
text_styles: default_text_styles(),
|
||||
drag_value_text_style: TextStyle::Button,
|
||||
wrap: None,
|
||||
spacing: Spacing::default(),
|
||||
interaction: Interaction::default(),
|
||||
@@ -646,11 +703,12 @@ impl Default for Spacing {
|
||||
Self {
|
||||
item_spacing: vec2(8.0, 3.0),
|
||||
window_margin: Margin::same(6.0),
|
||||
menu_margin: Margin::same(1.0),
|
||||
menu_margin: Margin::same(6.0),
|
||||
button_padding: vec2(4.0, 1.0),
|
||||
indent: 18.0, // match checkbox/radio-button with `button_padding.x + icon_width + icon_spacing`
|
||||
interact_size: vec2(40.0, 18.0),
|
||||
slider_width: 100.0,
|
||||
combo_width: 100.0,
|
||||
text_edit_width: 280.0,
|
||||
icon_width: 14.0,
|
||||
icon_width_inner: 8.0,
|
||||
@@ -658,6 +716,9 @@ impl Default for Spacing {
|
||||
tooltip_width: 600.0,
|
||||
combo_height: 200.0,
|
||||
scroll_bar_width: 8.0,
|
||||
scroll_handle_min_length: 12.0,
|
||||
scroll_bar_inner_margin: 4.0,
|
||||
scroll_bar_outer_margin: 0.0,
|
||||
indent_ends_with_horizontal_line: false,
|
||||
}
|
||||
}
|
||||
@@ -682,13 +743,21 @@ impl Visuals {
|
||||
widgets: Widgets::default(),
|
||||
selection: Selection::default(),
|
||||
hyperlink_color: Color32::from_rgb(90, 170, 255),
|
||||
faint_bg_color: Color32::from_gray(35),
|
||||
extreme_bg_color: Color32::from_gray(10), // e.g. TextEdit background
|
||||
faint_bg_color: Color32::from_additive_luminance(5), // visible, but barely so
|
||||
extreme_bg_color: Color32::from_gray(10), // e.g. TextEdit background
|
||||
code_bg_color: Color32::from_gray(64),
|
||||
warn_fg_color: Color32::from_rgb(255, 143, 0), // orange
|
||||
error_fg_color: Color32::from_rgb(255, 0, 0), // red
|
||||
|
||||
window_rounding: Rounding::same(6.0),
|
||||
window_shadow: Shadow::big_dark(),
|
||||
window_fill: Color32::from_gray(27),
|
||||
window_stroke: Stroke::new(1.0, Color32::from_gray(60)),
|
||||
|
||||
menu_rounding: Rounding::same(6.0),
|
||||
|
||||
panel_fill: Color32::from_gray(27),
|
||||
|
||||
popup_shadow: Shadow::small_dark(),
|
||||
resize_corner_size: 12.0,
|
||||
text_cursor_width: 2.0,
|
||||
@@ -696,6 +765,9 @@ impl Visuals {
|
||||
clip_rect_margin: 3.0, // should be at least half the size of the widest frame stroke + max WidgetVisuals::expansion
|
||||
button_frame: true,
|
||||
collapsing_header_frame: false,
|
||||
indent_has_left_vline: true,
|
||||
|
||||
striped: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -706,12 +778,18 @@ impl Visuals {
|
||||
widgets: Widgets::light(),
|
||||
selection: Selection::light(),
|
||||
hyperlink_color: Color32::from_rgb(0, 155, 255),
|
||||
faint_bg_color: Color32::from_gray(242),
|
||||
extreme_bg_color: Color32::from_gray(255), // e.g. TextEdit background
|
||||
faint_bg_color: Color32::from_additive_luminance(5), // visible, but barely so
|
||||
extreme_bg_color: Color32::from_gray(255), // e.g. TextEdit background
|
||||
code_bg_color: Color32::from_gray(230),
|
||||
warn_fg_color: Color32::from_rgb(255, 100, 0), // slightly orange red. it's difficult to find a warning color that pops on bright background.
|
||||
error_fg_color: Color32::from_rgb(255, 0, 0), // red
|
||||
|
||||
window_shadow: Shadow::big_light(),
|
||||
window_fill: Color32::from_gray(248),
|
||||
window_stroke: Stroke::new(1.0, Color32::from_gray(190)),
|
||||
|
||||
panel_fill: Color32::from_gray(248),
|
||||
|
||||
popup_shadow: Shadow::small_light(),
|
||||
..Self::dark()
|
||||
}
|
||||
@@ -750,20 +828,23 @@ impl Widgets {
|
||||
pub fn dark() -> Self {
|
||||
Self {
|
||||
noninteractive: WidgetVisuals {
|
||||
bg_fill: Color32::from_gray(27), // window background
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // separators, indentation lines, windows outlines
|
||||
weak_bg_fill: Color32::from_gray(27),
|
||||
bg_fill: Color32::from_gray(27),
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // separators, indentation lines
|
||||
fg_stroke: Stroke::new(1.0, Color32::from_gray(140)), // normal text color
|
||||
rounding: Rounding::same(2.0),
|
||||
expansion: 0.0,
|
||||
},
|
||||
inactive: WidgetVisuals {
|
||||
bg_fill: Color32::from_gray(60), // button background
|
||||
weak_bg_fill: Color32::from_gray(60), // button background
|
||||
bg_fill: Color32::from_gray(60), // checkbox background
|
||||
bg_stroke: Default::default(),
|
||||
fg_stroke: Stroke::new(1.0, Color32::from_gray(180)), // button text
|
||||
rounding: Rounding::same(2.0),
|
||||
expansion: 0.0,
|
||||
},
|
||||
hovered: WidgetVisuals {
|
||||
weak_bg_fill: Color32::from_gray(70),
|
||||
bg_fill: Color32::from_gray(70),
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(150)), // e.g. hover over window edge or button
|
||||
fg_stroke: Stroke::new(1.5, Color32::from_gray(240)),
|
||||
@@ -771,6 +852,7 @@ impl Widgets {
|
||||
expansion: 1.0,
|
||||
},
|
||||
active: WidgetVisuals {
|
||||
weak_bg_fill: Color32::from_gray(55),
|
||||
bg_fill: Color32::from_gray(55),
|
||||
bg_stroke: Stroke::new(1.0, Color32::WHITE),
|
||||
fg_stroke: Stroke::new(2.0, Color32::WHITE),
|
||||
@@ -778,6 +860,7 @@ impl Widgets {
|
||||
expansion: 1.0,
|
||||
},
|
||||
open: WidgetVisuals {
|
||||
weak_bg_fill: Color32::from_gray(27),
|
||||
bg_fill: Color32::from_gray(27),
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(60)),
|
||||
fg_stroke: Stroke::new(1.0, Color32::from_gray(210)),
|
||||
@@ -790,20 +873,23 @@ impl Widgets {
|
||||
pub fn light() -> Self {
|
||||
Self {
|
||||
noninteractive: WidgetVisuals {
|
||||
bg_fill: Color32::from_gray(248), // window background - should be distinct from TextEdit background
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(190)), // separators, indentation lines, windows outlines
|
||||
weak_bg_fill: Color32::from_gray(248),
|
||||
bg_fill: Color32::from_gray(248),
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(190)), // separators, indentation lines
|
||||
fg_stroke: Stroke::new(1.0, Color32::from_gray(80)), // normal text color
|
||||
rounding: Rounding::same(2.0),
|
||||
expansion: 0.0,
|
||||
},
|
||||
inactive: WidgetVisuals {
|
||||
bg_fill: Color32::from_gray(230), // button background
|
||||
weak_bg_fill: Color32::from_gray(230), // button background
|
||||
bg_fill: Color32::from_gray(230), // checkbox background
|
||||
bg_stroke: Default::default(),
|
||||
fg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // button text
|
||||
rounding: Rounding::same(2.0),
|
||||
expansion: 0.0,
|
||||
},
|
||||
hovered: WidgetVisuals {
|
||||
weak_bg_fill: Color32::from_gray(220),
|
||||
bg_fill: Color32::from_gray(220),
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(105)), // e.g. hover over window edge or button
|
||||
fg_stroke: Stroke::new(1.5, Color32::BLACK),
|
||||
@@ -811,6 +897,7 @@ impl Widgets {
|
||||
expansion: 1.0,
|
||||
},
|
||||
active: WidgetVisuals {
|
||||
weak_bg_fill: Color32::from_gray(165),
|
||||
bg_fill: Color32::from_gray(165),
|
||||
bg_stroke: Stroke::new(1.0, Color32::BLACK),
|
||||
fg_stroke: Stroke::new(2.0, Color32::BLACK),
|
||||
@@ -818,6 +905,7 @@ impl Widgets {
|
||||
expansion: 1.0,
|
||||
},
|
||||
open: WidgetVisuals {
|
||||
weak_bg_fill: Color32::from_gray(220),
|
||||
bg_fill: Color32::from_gray(220),
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(160)),
|
||||
fg_stroke: Stroke::new(1.0, Color32::BLACK),
|
||||
@@ -844,6 +932,7 @@ impl Style {
|
||||
override_font_id,
|
||||
override_text_style,
|
||||
text_styles,
|
||||
drag_value_text_style,
|
||||
wrap: _,
|
||||
spacing,
|
||||
interaction,
|
||||
@@ -885,6 +974,19 @@ impl Style {
|
||||
});
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Text style of DragValue:");
|
||||
crate::ComboBox::from_id_source("drag_value_text_style")
|
||||
.selected_text(drag_value_text_style.to_string())
|
||||
.show_ui(ui, |ui| {
|
||||
let all_text_styles = ui.style().text_styles();
|
||||
for style in all_text_styles {
|
||||
let text =
|
||||
crate::RichText::new(style.to_string()).text_style(style.clone());
|
||||
ui.selectable_value(drag_value_text_style, style, text);
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Animation duration:");
|
||||
ui.add(
|
||||
Slider::new(animation_time, 0.0..=1.0)
|
||||
@@ -933,6 +1035,7 @@ impl Spacing {
|
||||
indent,
|
||||
interact_size,
|
||||
slider_width,
|
||||
combo_width,
|
||||
text_edit_width,
|
||||
icon_width,
|
||||
icon_width_inner,
|
||||
@@ -941,68 +1044,15 @@ impl Spacing {
|
||||
indent_ends_with_horizontal_line,
|
||||
combo_height,
|
||||
scroll_bar_width,
|
||||
scroll_handle_min_length,
|
||||
scroll_bar_inner_margin,
|
||||
scroll_bar_outer_margin,
|
||||
} = self;
|
||||
|
||||
ui.add(slider_vec2(item_spacing, 0.0..=20.0, "Item spacing"));
|
||||
|
||||
let margin_range = 0.0..=20.0;
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(
|
||||
DragValue::new(&mut window_margin.left)
|
||||
.clamp_range(margin_range.clone())
|
||||
.prefix("left: "),
|
||||
);
|
||||
ui.add(
|
||||
DragValue::new(&mut window_margin.right)
|
||||
.clamp_range(margin_range.clone())
|
||||
.prefix("right: "),
|
||||
);
|
||||
|
||||
ui.label("Window margins x");
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(
|
||||
DragValue::new(&mut window_margin.top)
|
||||
.clamp_range(margin_range.clone())
|
||||
.prefix("top: "),
|
||||
);
|
||||
ui.add(
|
||||
DragValue::new(&mut window_margin.bottom)
|
||||
.clamp_range(margin_range.clone())
|
||||
.prefix("bottom: "),
|
||||
);
|
||||
ui.label("Window margins y");
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(
|
||||
DragValue::new(&mut menu_margin.left)
|
||||
.clamp_range(margin_range.clone())
|
||||
.prefix("left: "),
|
||||
);
|
||||
ui.add(
|
||||
DragValue::new(&mut menu_margin.right)
|
||||
.clamp_range(margin_range.clone())
|
||||
.prefix("right: "),
|
||||
);
|
||||
|
||||
ui.label("Menu margins x");
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(
|
||||
DragValue::new(&mut menu_margin.top)
|
||||
.clamp_range(margin_range.clone())
|
||||
.prefix("top: "),
|
||||
);
|
||||
ui.add(
|
||||
DragValue::new(&mut menu_margin.bottom)
|
||||
.clamp_range(margin_range)
|
||||
.prefix("bottom: "),
|
||||
);
|
||||
ui.label("Menu margins y");
|
||||
});
|
||||
margin_ui(ui, "Window margin:", window_margin);
|
||||
margin_ui(ui, "Menu margin:", menu_margin);
|
||||
|
||||
ui.add(slider_vec2(button_padding, 0.0..=20.0, "Button padding"));
|
||||
ui.add(slider_vec2(interact_size, 4.0..=60.0, "Interact size"))
|
||||
@@ -1015,13 +1065,29 @@ impl Spacing {
|
||||
ui.add(DragValue::new(slider_width).clamp_range(0.0..=1000.0));
|
||||
ui.label("Slider width");
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(DragValue::new(combo_width).clamp_range(0.0..=1000.0));
|
||||
ui.label("ComboBox width");
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(DragValue::new(text_edit_width).clamp_range(0.0..=1000.0));
|
||||
ui.label("TextEdit width");
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(DragValue::new(scroll_bar_width).clamp_range(0.0..=32.0));
|
||||
ui.label("Scroll-bar width width");
|
||||
ui.label("Scroll-bar width");
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(DragValue::new(scroll_handle_min_length).clamp_range(0.0..=32.0));
|
||||
ui.label("Scroll-bar handle min length");
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(DragValue::new(scroll_bar_inner_margin).clamp_range(0.0..=32.0));
|
||||
ui.label("Scroll-bar inner margin");
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(DragValue::new(scroll_bar_outer_margin).clamp_range(0.0..=32.0));
|
||||
ui.label("Scroll-bar outer margin");
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
@@ -1062,6 +1128,55 @@ impl Spacing {
|
||||
}
|
||||
}
|
||||
|
||||
fn margin_ui(ui: &mut Ui, text: &str, margin: &mut Margin) {
|
||||
let margin_range = 0.0..=20.0;
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(text);
|
||||
|
||||
let mut same = margin.is_same();
|
||||
ui.checkbox(&mut same, "Same");
|
||||
|
||||
if same {
|
||||
let mut value = margin.left;
|
||||
ui.add(DragValue::new(&mut value).clamp_range(margin_range.clone()));
|
||||
*margin = Margin::same(value);
|
||||
} else {
|
||||
if margin.is_same() {
|
||||
// HACK: prevent collapse:
|
||||
margin.right = margin.left + 1.0;
|
||||
margin.bottom = margin.left + 2.0;
|
||||
margin.top = margin.left + 3.0;
|
||||
}
|
||||
|
||||
ui.add(
|
||||
DragValue::new(&mut margin.left)
|
||||
.clamp_range(margin_range.clone())
|
||||
.prefix("L: "),
|
||||
)
|
||||
.on_hover_text("Left margin");
|
||||
ui.add(
|
||||
DragValue::new(&mut margin.right)
|
||||
.clamp_range(margin_range.clone())
|
||||
.prefix("R: "),
|
||||
)
|
||||
.on_hover_text("Right margin");
|
||||
ui.add(
|
||||
DragValue::new(&mut margin.top)
|
||||
.clamp_range(margin_range.clone())
|
||||
.prefix("T: "),
|
||||
)
|
||||
.on_hover_text("Top margin");
|
||||
ui.add(
|
||||
DragValue::new(&mut margin.bottom)
|
||||
.clamp_range(margin_range)
|
||||
.prefix("B: "),
|
||||
)
|
||||
.on_hover_text("Bottom margin");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
impl Interaction {
|
||||
pub fn ui(&mut self, ui: &mut crate::Ui) {
|
||||
let Self {
|
||||
@@ -1131,13 +1246,17 @@ impl Selection {
|
||||
impl WidgetVisuals {
|
||||
pub fn ui(&mut self, ui: &mut crate::Ui) {
|
||||
let Self {
|
||||
bg_fill,
|
||||
weak_bg_fill,
|
||||
bg_fill: mandatory_bg_fill,
|
||||
bg_stroke,
|
||||
rounding,
|
||||
fg_stroke,
|
||||
expansion,
|
||||
} = self;
|
||||
ui_color(ui, bg_fill, "background fill");
|
||||
ui_color(ui, weak_bg_fill, "optional background fill")
|
||||
.on_hover_text("For buttons, combo-boxes, etc");
|
||||
ui_color(ui, mandatory_bg_fill, "mandatory background fill")
|
||||
.on_hover_text("For checkboxes, sliders, etc");
|
||||
stroke_ui(ui, bg_stroke, "background stroke");
|
||||
|
||||
rounding_ui(ui, rounding);
|
||||
@@ -1193,20 +1312,33 @@ impl Visuals {
|
||||
code_bg_color,
|
||||
warn_fg_color,
|
||||
error_fg_color,
|
||||
|
||||
window_rounding,
|
||||
window_shadow,
|
||||
window_fill,
|
||||
window_stroke,
|
||||
|
||||
menu_rounding,
|
||||
|
||||
panel_fill,
|
||||
|
||||
popup_shadow,
|
||||
|
||||
resize_corner_size,
|
||||
text_cursor_width,
|
||||
text_cursor_preview,
|
||||
clip_rect_margin,
|
||||
button_frame,
|
||||
collapsing_header_frame,
|
||||
indent_has_left_vline,
|
||||
|
||||
striped,
|
||||
} = self;
|
||||
|
||||
ui.collapsing("Background Colors", |ui| {
|
||||
ui_color(ui, &mut widgets.inactive.bg_fill, "Buttons");
|
||||
ui_color(ui, &mut widgets.noninteractive.bg_fill, "Windows");
|
||||
ui_color(ui, &mut widgets.inactive.weak_bg_fill, "Buttons");
|
||||
ui_color(ui, window_fill, "Windows");
|
||||
ui_color(ui, panel_fill, "Panels");
|
||||
ui_color(ui, faint_bg_color, "Faint accent").on_hover_text(
|
||||
"Used for faint accentuation of interactive things, like striped grids.",
|
||||
);
|
||||
@@ -1215,14 +1347,15 @@ impl Visuals {
|
||||
});
|
||||
|
||||
ui.collapsing("Window", |ui| {
|
||||
// Common shortcuts
|
||||
ui_color(ui, &mut widgets.noninteractive.bg_fill, "Fill");
|
||||
stroke_ui(ui, &mut widgets.noninteractive.bg_stroke, "Outline");
|
||||
|
||||
ui_color(ui, window_fill, "Fill");
|
||||
stroke_ui(ui, window_stroke, "Outline");
|
||||
rounding_ui(ui, window_rounding);
|
||||
|
||||
shadow_ui(ui, window_shadow, "Shadow");
|
||||
shadow_ui(ui, popup_shadow, "Shadow (small menus and popups)");
|
||||
});
|
||||
|
||||
ui.collapsing("Menus and popups", |ui| {
|
||||
rounding_ui(ui, menu_rounding);
|
||||
shadow_ui(ui, popup_shadow, "Shadow");
|
||||
});
|
||||
|
||||
ui.collapsing("Widgets", |ui| widgets.ui(ui));
|
||||
@@ -1255,6 +1388,12 @@ impl Visuals {
|
||||
|
||||
ui.checkbox(button_frame, "Button has a frame");
|
||||
ui.checkbox(collapsing_header_frame, "Collapsing header has a frame");
|
||||
ui.checkbox(
|
||||
indent_has_left_vline,
|
||||
"Paint a vertical line to the left of indented regions",
|
||||
);
|
||||
|
||||
ui.checkbox(striped, "By default, add stripes to grids and tables?");
|
||||
|
||||
ui.vertical_centered(|ui| reset_button(ui, self));
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
use std::hash::Hash;
|
||||
use std::sync::Arc;
|
||||
|
||||
use epaint::mutex::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
use epaint::mutex::RwLock;
|
||||
|
||||
use crate::{
|
||||
color::*, containers::*, epaint::text::Fonts, layout::*, menu::MenuState, placer::Placer,
|
||||
widgets::*, *,
|
||||
containers::*, ecolor::*, epaint::text::Fonts, layout::*, menu::MenuState, placer::Placer,
|
||||
util::IdTypeMap, widgets::*, *,
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -239,7 +239,7 @@ impl Ui {
|
||||
self.enabled &= enabled;
|
||||
if !self.enabled && self.is_visible() {
|
||||
self.painter
|
||||
.set_fade_to_color(Some(self.visuals().window_fill()));
|
||||
.set_fade_to_color(Some(self.visuals().fade_out_to_color()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,84 +314,9 @@ impl Ui {
|
||||
self.painter().layer_id()
|
||||
}
|
||||
|
||||
/// The [`InputState`] of the [`Context`] associated with this [`Ui`].
|
||||
/// Equivalent to `.ctx().input()`.
|
||||
///
|
||||
/// Note that this locks the [`Context`], so be careful with if-let bindings:
|
||||
///
|
||||
/// ```
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
/// if let Some(pos) = { ui.input().pointer.hover_pos() } {
|
||||
/// // This is fine!
|
||||
/// }
|
||||
///
|
||||
/// let pos = ui.input().pointer.hover_pos();
|
||||
/// if let Some(pos) = pos {
|
||||
/// // This is also fine!
|
||||
/// }
|
||||
///
|
||||
/// if let Some(pos) = ui.input().pointer.hover_pos() {
|
||||
/// // ⚠️ Using `ui` again here will lead to a dead-lock!
|
||||
/// }
|
||||
/// # });
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn input(&self) -> RwLockReadGuard<'_, InputState> {
|
||||
self.ctx().input()
|
||||
}
|
||||
|
||||
/// The [`InputState`] of the [`Context`] associated with this [`Ui`].
|
||||
/// Equivalent to `.ctx().input_mut()`.
|
||||
///
|
||||
/// Note that this locks the [`Context`], so be careful with if-let bindings
|
||||
/// like for [`Self::input()`].
|
||||
/// ```
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
/// ui.input_mut().consume_key(egui::Modifiers::default(), egui::Key::Enter);
|
||||
/// # });
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn input_mut(&self) -> RwLockWriteGuard<'_, InputState> {
|
||||
self.ctx().input_mut()
|
||||
}
|
||||
|
||||
/// The [`Memory`] of the [`Context`] associated with this ui.
|
||||
/// Equivalent to `.ctx().memory()`.
|
||||
#[inline]
|
||||
pub fn memory(&self) -> RwLockWriteGuard<'_, Memory> {
|
||||
self.ctx().memory()
|
||||
}
|
||||
|
||||
/// Stores superficial widget state.
|
||||
#[inline]
|
||||
pub fn data(&self) -> RwLockWriteGuard<'_, crate::util::IdTypeMap> {
|
||||
self.ctx().data()
|
||||
}
|
||||
|
||||
/// The [`PlatformOutput`] of the [`Context`] associated with this ui.
|
||||
/// Equivalent to `.ctx().output()`.
|
||||
///
|
||||
/// ```
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
/// if ui.button("📋").clicked() {
|
||||
/// ui.output().copied_text = "some_text".to_string();
|
||||
/// }
|
||||
/// # });
|
||||
#[inline]
|
||||
pub fn output(&self) -> RwLockWriteGuard<'_, PlatformOutput> {
|
||||
self.ctx().output()
|
||||
}
|
||||
|
||||
/// The [`Fonts`] of the [`Context`] associated with this ui.
|
||||
/// Equivalent to `.ctx().fonts()`.
|
||||
#[inline]
|
||||
pub fn fonts(&self) -> RwLockReadGuard<'_, Fonts> {
|
||||
self.ctx().fonts()
|
||||
}
|
||||
|
||||
/// The height of text of this text style
|
||||
pub fn text_style_height(&self, style: &TextStyle) -> f32 {
|
||||
self.fonts().row_height(&style.resolve(self.style()))
|
||||
self.fonts(|f| f.row_height(&style.resolve(self.style())))
|
||||
}
|
||||
|
||||
/// Screen-space rectangle for clipping what we paint in this ui.
|
||||
@@ -413,6 +338,87 @@ impl Ui {
|
||||
}
|
||||
}
|
||||
|
||||
/// # Helpers for accessing the underlying [`Context`].
|
||||
/// These functions all lock the [`Context`] owned by this [`Ui`].
|
||||
/// Please see the documentation of [`Context`] for how locking works!
|
||||
impl Ui {
|
||||
/// Read-only access to the shared [`InputState`].
|
||||
///
|
||||
/// ```
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
/// if ui.input(|i| i.key_pressed(egui::Key::A)) {
|
||||
/// // …
|
||||
/// }
|
||||
/// # });
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn input<R>(&self, reader: impl FnOnce(&InputState) -> R) -> R {
|
||||
self.ctx().input(reader)
|
||||
}
|
||||
|
||||
/// Read-write access to the shared [`InputState`].
|
||||
#[inline]
|
||||
pub fn input_mut<R>(&self, writer: impl FnOnce(&mut InputState) -> R) -> R {
|
||||
self.ctx().input_mut(writer)
|
||||
}
|
||||
|
||||
/// Read-only access to the shared [`Memory`].
|
||||
#[inline]
|
||||
pub fn memory<R>(&self, reader: impl FnOnce(&Memory) -> R) -> R {
|
||||
self.ctx().memory(reader)
|
||||
}
|
||||
|
||||
/// Read-write access to the shared [`Memory`].
|
||||
#[inline]
|
||||
pub fn memory_mut<R>(&self, writer: impl FnOnce(&mut Memory) -> R) -> R {
|
||||
self.ctx().memory_mut(writer)
|
||||
}
|
||||
|
||||
/// Read-only access to the shared [`IdTypeMap`], which stores superficial widget state.
|
||||
#[inline]
|
||||
pub fn data<R>(&self, reader: impl FnOnce(&IdTypeMap) -> R) -> R {
|
||||
self.ctx().data(reader)
|
||||
}
|
||||
|
||||
/// Read-write access to the shared [`IdTypeMap`], which stores superficial widget state.
|
||||
#[inline]
|
||||
pub fn data_mut<R>(&self, writer: impl FnOnce(&mut IdTypeMap) -> R) -> R {
|
||||
self.ctx().data_mut(writer)
|
||||
}
|
||||
|
||||
/// Read-only access to the shared [`PlatformOutput`].
|
||||
///
|
||||
/// This is what egui outputs each frame.
|
||||
///
|
||||
/// ```
|
||||
/// # let mut ctx = egui::Context::default();
|
||||
/// ctx.output_mut(|o| o.cursor_icon = egui::CursorIcon::Progress);
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn output<R>(&self, reader: impl FnOnce(&PlatformOutput) -> R) -> R {
|
||||
self.ctx().output(reader)
|
||||
}
|
||||
|
||||
/// Read-write access to the shared [`PlatformOutput`].
|
||||
///
|
||||
/// This is what egui outputs each frame.
|
||||
///
|
||||
/// ```
|
||||
/// # let mut ctx = egui::Context::default();
|
||||
/// ctx.output_mut(|o| o.cursor_icon = egui::CursorIcon::Progress);
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn output_mut<R>(&self, writer: impl FnOnce(&mut PlatformOutput) -> R) -> R {
|
||||
self.ctx().output_mut(writer)
|
||||
}
|
||||
|
||||
/// Read-only access to [`Fonts`].
|
||||
#[inline]
|
||||
pub fn fonts<R>(&self, reader: impl FnOnce(&Fonts) -> R) -> R {
|
||||
self.ctx().fonts(reader)
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/// # Sizes etc
|
||||
@@ -961,7 +967,8 @@ 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];
|
||||
self.ctx().frame_state().scroll_target[d] = Some((range, align));
|
||||
self.ctx()
|
||||
.frame_state_mut(|state| state.scroll_target[d] = Some((range, align)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -990,7 +997,8 @@ impl Ui {
|
||||
let target = self.next_widget_position();
|
||||
for d in 0..2 {
|
||||
let target = target[d];
|
||||
self.ctx().frame_state().scroll_target[d] = Some((target..=target, align));
|
||||
self.ctx()
|
||||
.frame_state_mut(|state| state.scroll_target[d] = Some((target..=target, align)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1022,7 +1030,8 @@ impl Ui {
|
||||
/// # });
|
||||
/// ```
|
||||
pub fn scroll_with_delta(&self, delta: Vec2) {
|
||||
self.ctx().frame_state().scroll_delta += delta;
|
||||
self.ctx()
|
||||
.frame_state_mut(|state| state.scroll_delta += delta);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1447,7 +1456,7 @@ impl Ui {
|
||||
text: impl Into<WidgetText>,
|
||||
) -> Response {
|
||||
let mut response = self.radio(*current_value == alternative, text);
|
||||
if response.clicked() {
|
||||
if response.clicked() && *current_value != alternative {
|
||||
*current_value = alternative;
|
||||
response.mark_changed();
|
||||
}
|
||||
@@ -1475,7 +1484,7 @@ impl Ui {
|
||||
text: impl Into<WidgetText>,
|
||||
) -> Response {
|
||||
let mut response = self.selectable_label(*current_value == selected_value, text);
|
||||
if response.clicked() {
|
||||
if response.clicked() && *current_value != selected_value {
|
||||
*current_value = selected_value;
|
||||
response.mark_changed();
|
||||
}
|
||||
@@ -1563,7 +1572,7 @@ impl Ui {
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Se also [`crate::Image`] and [`crate::ImageButton`].
|
||||
/// See also [`crate::Image`] and [`crate::ImageButton`].
|
||||
#[inline]
|
||||
pub fn image(&mut self, texture_id: impl Into<TextureId>, size: impl Into<Vec2>) -> Response {
|
||||
Image::new(texture_id, size).ui(self)
|
||||
@@ -1780,24 +1789,31 @@ impl Ui {
|
||||
};
|
||||
let ret = add_contents(&mut child_ui);
|
||||
|
||||
let left_vline = self.visuals().indent_has_left_vline;
|
||||
let end_with_horizontal_line = self.spacing().indent_ends_with_horizontal_line;
|
||||
|
||||
if end_with_horizontal_line {
|
||||
child_ui.add_space(4.0);
|
||||
}
|
||||
if left_vline || end_with_horizontal_line {
|
||||
if end_with_horizontal_line {
|
||||
child_ui.add_space(4.0);
|
||||
}
|
||||
|
||||
// draw a faint line on the left to mark the indented section
|
||||
let stroke = self.visuals().widgets.noninteractive.bg_stroke;
|
||||
let left_top = child_rect.min - 0.5 * indent * Vec2::X;
|
||||
let left_top = self.painter().round_pos_to_pixels(left_top);
|
||||
let left_bottom = pos2(left_top.x, child_ui.min_rect().bottom() - 2.0);
|
||||
let left_bottom = self.painter().round_pos_to_pixels(left_bottom);
|
||||
self.painter.line_segment([left_top, left_bottom], stroke);
|
||||
if end_with_horizontal_line {
|
||||
let fudge = 2.0; // looks nicer with button rounding in collapsing headers
|
||||
let right_bottom = pos2(child_ui.min_rect().right() - fudge, left_bottom.y);
|
||||
self.painter
|
||||
.line_segment([left_bottom, right_bottom], stroke);
|
||||
let stroke = self.visuals().widgets.noninteractive.bg_stroke;
|
||||
let left_top = child_rect.min - 0.5 * indent * Vec2::X;
|
||||
let left_top = self.painter().round_pos_to_pixels(left_top);
|
||||
let left_bottom = pos2(left_top.x, child_ui.min_rect().bottom() - 2.0);
|
||||
let left_bottom = self.painter().round_pos_to_pixels(left_bottom);
|
||||
|
||||
if left_vline {
|
||||
// draw a faint line on the left to mark the indented section
|
||||
self.painter.line_segment([left_top, left_bottom], stroke);
|
||||
}
|
||||
|
||||
if end_with_horizontal_line {
|
||||
let fudge = 2.0; // looks nicer with button rounding in collapsing headers
|
||||
let right_bottom = pos2(child_ui.min_rect().right() - fudge, left_bottom.y);
|
||||
self.painter
|
||||
.line_segment([left_bottom, right_bottom], stroke);
|
||||
}
|
||||
}
|
||||
|
||||
let response = self.allocate_rect(child_ui.min_rect(), Sense::hover());
|
||||
@@ -2007,12 +2023,14 @@ impl Ui {
|
||||
InnerResponse::new(inner, self.interact(rect, child_ui.id, Sense::hover()))
|
||||
}
|
||||
|
||||
/// This will make the next added widget centered in the available space.
|
||||
#[deprecated = "Use ui.vertical_centered or ui.centered_and_justified"]
|
||||
pub fn centered<R>(&mut self, add_contents: impl FnOnce(&mut Self) -> R) -> InnerResponse<R> {
|
||||
self.with_layout_dyn(Layout::centered(Direction::TopDown), Box::new(add_contents))
|
||||
self.vertical_centered(add_contents)
|
||||
}
|
||||
|
||||
/// This will make the next added widget centered and justified in the available space.
|
||||
///
|
||||
/// Only one widget may be added to the inner `Ui`!
|
||||
pub fn centered_and_justified<R>(
|
||||
&mut self,
|
||||
add_contents: impl FnOnce(&mut Self) -> R,
|
||||
@@ -2155,6 +2173,43 @@ impl Ui {
|
||||
menu::menu_button(self, title, add_contents)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Create a menu button with an image that when clicked will show the given menu.
|
||||
///
|
||||
/// If called from within a menu this will instead create a button for a sub-menu.
|
||||
///
|
||||
/// ```ignore
|
||||
/// use egui_extras;
|
||||
///
|
||||
/// let img = egui_extras::RetainedImage::from_svg_bytes_with_size(
|
||||
/// "rss",
|
||||
/// include_bytes!("rss.svg"),
|
||||
/// egui_extras::image::FitTo::Size(24, 24),
|
||||
/// );
|
||||
///
|
||||
/// ui.menu_image_button(img.texture_id(ctx), img.size_vec2(), |ui| {
|
||||
/// ui.menu_button("My sub-menu", |ui| {
|
||||
/// if ui.button("Close the menu").clicked() {
|
||||
/// ui.close_menu();
|
||||
/// }
|
||||
/// });
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// See also: [`Self::close_menu`] and [`Response::context_menu`].
|
||||
pub fn menu_image_button<R>(
|
||||
&mut self,
|
||||
texture_id: TextureId,
|
||||
image_size: impl Into<Vec2>,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<Option<R>> {
|
||||
if let Some(menu_state) = self.menu_state.clone() {
|
||||
menu::submenu_button(self, menu_state, String::new(), add_contents)
|
||||
} else {
|
||||
menu::menu_image_button(self, texture_id, image_size, add_contents)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
@@ -460,18 +460,18 @@ impl IdTypeMap {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_empty(&mut self) -> bool {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn len(&mut self) -> usize {
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
/// Count how many values are stored but not yet deserialized.
|
||||
#[inline]
|
||||
pub fn count_serialized(&mut self) -> usize {
|
||||
pub fn count_serialized(&self) -> usize {
|
||||
self.0
|
||||
.values()
|
||||
.filter(|e| matches!(e, Element::Serialized { .. }))
|
||||
@@ -479,7 +479,7 @@ impl IdTypeMap {
|
||||
}
|
||||
|
||||
/// Count the number of values are stored with the given type.
|
||||
pub fn count<T: 'static>(&mut self) -> usize {
|
||||
pub fn count<T: 'static>(&self) -> usize {
|
||||
let key = TypeId::of::<T>();
|
||||
self.0
|
||||
.iter()
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
|
||||
pub mod cache;
|
||||
pub(crate) mod fixed_cache;
|
||||
mod history;
|
||||
pub mod id_type_map;
|
||||
pub mod undoer;
|
||||
|
||||
pub use history::History;
|
||||
pub use id_type_map::IdTypeMap;
|
||||
|
||||
pub use epaint::emath::History;
|
||||
pub use epaint::util::{hash, hash_with};
|
||||
|
||||
@@ -293,12 +293,12 @@ impl RichText {
|
||||
let underline = if underline {
|
||||
crate::Stroke::new(1.0, line_color)
|
||||
} else {
|
||||
crate::Stroke::none()
|
||||
crate::Stroke::NONE
|
||||
};
|
||||
let strikethrough = if strikethrough {
|
||||
crate::Stroke::new(1.0, line_color)
|
||||
} else {
|
||||
crate::Stroke::none()
|
||||
crate::Stroke::NONE
|
||||
};
|
||||
|
||||
let valign = if raised {
|
||||
@@ -573,14 +573,14 @@ impl WidgetText {
|
||||
let mut text_job = text.into_text_job(ui.style(), fallback_font.into(), valign);
|
||||
text_job.job.wrap.max_width = wrap_width;
|
||||
WidgetTextGalley {
|
||||
galley: ui.fonts().layout_job(text_job.job),
|
||||
galley: ui.fonts(|f| f.layout_job(text_job.job)),
|
||||
galley_has_color: text_job.job_has_color,
|
||||
}
|
||||
}
|
||||
Self::LayoutJob(mut job) => {
|
||||
job.wrap.max_width = wrap_width;
|
||||
WidgetTextGalley {
|
||||
galley: ui.fonts().layout_job(job),
|
||||
galley: ui.fonts(|f| f.layout_job(job)),
|
||||
galley_has_color: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ pub struct Button {
|
||||
small: bool,
|
||||
frame: Option<bool>,
|
||||
min_size: Vec2,
|
||||
rounding: Option<Rounding>,
|
||||
image: Option<widgets::Image>,
|
||||
}
|
||||
|
||||
@@ -45,6 +46,7 @@ impl Button {
|
||||
small: false,
|
||||
frame: None,
|
||||
min_size: Vec2::ZERO,
|
||||
rounding: None,
|
||||
image: None,
|
||||
}
|
||||
}
|
||||
@@ -117,6 +119,12 @@ impl Button {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the rounding of the button.
|
||||
pub fn rounding(mut self, rounding: impl Into<Rounding>) -> Self {
|
||||
self.rounding = Some(rounding.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Show some text on the right side of the button, in weak color.
|
||||
///
|
||||
/// Designed for menu buttons, for setting a keyboard shortcut text (e.g. `Ctrl+S`).
|
||||
@@ -140,6 +148,7 @@ impl Widget for Button {
|
||||
small,
|
||||
frame,
|
||||
min_size,
|
||||
rounding,
|
||||
image,
|
||||
} = self;
|
||||
|
||||
@@ -171,10 +180,10 @@ impl Widget for Button {
|
||||
desired_size.x += ui.spacing().item_spacing.x + shortcut_text.size().x;
|
||||
desired_size.y = desired_size.y.max(shortcut_text.size().y);
|
||||
}
|
||||
desired_size += 2.0 * button_padding;
|
||||
if !small {
|
||||
desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y);
|
||||
}
|
||||
desired_size += 2.0 * button_padding;
|
||||
desired_size = desired_size.at_least(min_size);
|
||||
|
||||
let (rect, response) = ui.allocate_at_least(desired_size, sense);
|
||||
@@ -184,14 +193,11 @@ impl Widget for Button {
|
||||
let visuals = ui.style().interact(&response);
|
||||
|
||||
if frame {
|
||||
let fill = fill.unwrap_or(visuals.bg_fill);
|
||||
let fill = fill.unwrap_or(visuals.weak_bg_fill);
|
||||
let stroke = stroke.unwrap_or(visuals.bg_stroke);
|
||||
ui.painter().rect(
|
||||
rect.expand(visuals.expansion),
|
||||
visuals.rounding,
|
||||
fill,
|
||||
stroke,
|
||||
);
|
||||
let rounding = rounding.unwrap_or(visuals.rounding);
|
||||
ui.painter()
|
||||
.rect(rect.expand(visuals.expansion), rounding, fill, stroke);
|
||||
}
|
||||
|
||||
let text_pos = if let Some(image) = image {
|
||||
@@ -221,7 +227,10 @@ impl Widget for Button {
|
||||
|
||||
if let Some(image) = image {
|
||||
let image_rect = Rect::from_min_size(
|
||||
pos2(rect.min.x, rect.center().y - 0.5 - (image.size().y / 2.0)),
|
||||
pos2(
|
||||
rect.min.x + button_padding.x,
|
||||
rect.center().y - 0.5 - (image.size().y / 2.0),
|
||||
),
|
||||
image.size(),
|
||||
);
|
||||
image.paint_at(ui, image_rect);
|
||||
@@ -531,7 +540,7 @@ impl Widget for ImageButton {
|
||||
(
|
||||
expansion,
|
||||
visuals.rounding,
|
||||
visuals.bg_fill,
|
||||
visuals.weak_bg_fill,
|
||||
visuals.bg_stroke,
|
||||
)
|
||||
} else {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use crate::util::fixed_cache::FixedCache;
|
||||
use crate::*;
|
||||
use epaint::{color::*, *};
|
||||
use epaint::{ecolor::*, *};
|
||||
|
||||
fn contrast_color(color: impl Into<Rgba>) -> Color32 {
|
||||
if color.into().intensity() < 0.5 {
|
||||
@@ -211,7 +211,7 @@ fn color_slider_2d(
|
||||
}
|
||||
|
||||
/// What options to show for alpha
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Alpha {
|
||||
// Set alpha to 1.0, and show no option for it.
|
||||
Opaque,
|
||||
@@ -228,9 +228,9 @@ 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().copied_text = format!("{}, {}, {}", r, g, b);
|
||||
ui.output_mut(|o| o.copied_text = format!("{}, {}, {}", r, g, b));
|
||||
} else {
|
||||
ui.output().copied_text = format!("{}, {}, {}, {}", r, g, b, a);
|
||||
ui.output_mut(|o| o.copied_text = format!("{}, {}, {}, {}", r, g, b, a));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,20 +341,20 @@ pub fn color_picker_color32(ui: &mut Ui, srgba: &mut Color32, alpha: Alpha) -> b
|
||||
|
||||
pub fn color_edit_button_hsva(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) -> Response {
|
||||
let popup_id = ui.auto_id_with("popup");
|
||||
let open = ui.memory().is_popup_open(popup_id);
|
||||
let open = ui.memory(|mem| mem.is_popup_open(popup_id));
|
||||
let mut button_response = color_button(ui, (*hsva).into(), open);
|
||||
if ui.style().explanation_tooltips {
|
||||
button_response = button_response.on_hover_text("Click to edit color");
|
||||
}
|
||||
|
||||
if button_response.clicked() {
|
||||
ui.memory().toggle_popup(popup_id);
|
||||
ui.memory_mut(|mem| mem.toggle_popup(popup_id));
|
||||
}
|
||||
|
||||
const COLOR_SLIDER_WIDTH: f32 = 210.0;
|
||||
|
||||
// TODO(emilk): make it easier to show a temporary popup that closes when you click outside it
|
||||
if ui.memory().is_popup_open(popup_id) {
|
||||
if ui.memory(|mem| mem.is_popup_open(popup_id)) {
|
||||
let area_response = Area::new(popup_id)
|
||||
.order(Order::Foreground)
|
||||
.fixed_pos(button_response.rect.max)
|
||||
@@ -370,9 +370,9 @@ pub fn color_edit_button_hsva(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) -> Res
|
||||
.response;
|
||||
|
||||
if !button_response.clicked()
|
||||
&& (ui.input().key_pressed(Key::Escape) || area_response.clicked_elsewhere())
|
||||
&& (ui.input(|i| i.key_pressed(Key::Escape)) || area_response.clicked_elsewhere())
|
||||
{
|
||||
ui.memory().close_popup();
|
||||
ui.memory_mut(|mem| mem.close_popup());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -425,7 +425,7 @@ pub fn color_edit_button_rgb(ui: &mut Ui, rgb: &mut [f32; 3]) -> Response {
|
||||
// To ensure we keep hue slider when `srgba` is gray we store the full [`Hsva`] in a cache:
|
||||
fn color_cache_get(ctx: &Context, rgba: impl Into<Rgba>) -> Hsva {
|
||||
let rgba = rgba.into();
|
||||
use_color_cache(ctx, |cc| cc.get(&rgba).cloned()).unwrap_or_else(|| Hsva::from(rgba))
|
||||
use_color_cache(ctx, |cc| cc.get(&rgba).copied()).unwrap_or_else(|| Hsva::from(rgba))
|
||||
}
|
||||
|
||||
// To ensure we keep hue slider when `srgba` is gray we store the full [`Hsva`] in a cache:
|
||||
@@ -436,5 +436,5 @@ fn color_cache_set(ctx: &Context, rgba: impl Into<Rgba>, hsva: Hsva) {
|
||||
|
||||
// To ensure we keep hue slider when `srgba` is gray we store the full [`Hsva`] in a cache:
|
||||
fn use_color_cache<R>(ctx: &Context, f: impl FnOnce(&mut FixedCache<Rgba, Hsva>) -> R) -> R {
|
||||
f(ctx.data().get_temp_mut_or_default(Id::null()))
|
||||
ctx.data_mut(|d| f(d.get_temp_mut_or_default(Id::null())))
|
||||
}
|
||||
|
||||
@@ -368,21 +368,83 @@ impl<'a> Widget for DragValue<'a> {
|
||||
custom_parser,
|
||||
} = self;
|
||||
|
||||
let shift = ui.input().modifiers.shift_only();
|
||||
let is_slow_speed = shift && ui.memory().is_being_dragged(ui.next_auto_id());
|
||||
let shift = ui.input(|i| i.modifiers.shift_only());
|
||||
// The widget has the same ID whether it's in edit or button mode.
|
||||
let id = ui.next_auto_id();
|
||||
let is_slow_speed = shift && ui.memory(|mem| mem.is_being_dragged(id));
|
||||
|
||||
// The following ensures that when a `DragValue` receives focus,
|
||||
// it is immediately rendered in edit mode, rather than being rendered
|
||||
// in button mode for just one frame. This is important for
|
||||
// screen readers.
|
||||
let is_kb_editing = ui.memory_mut(|mem| {
|
||||
mem.interested_in_focus(id);
|
||||
let is_kb_editing = mem.has_focus(id);
|
||||
if mem.gained_focus(id) {
|
||||
mem.drag_value.edit_string = None;
|
||||
}
|
||||
is_kb_editing
|
||||
});
|
||||
|
||||
let old_value = get(&mut get_set_value);
|
||||
let value = clamp_to_range(old_value, clamp_range.clone());
|
||||
if old_value != value {
|
||||
set(&mut get_set_value, value);
|
||||
}
|
||||
let aim_rad = ui.input().aim_radius() as f64;
|
||||
let mut value = old_value;
|
||||
let aim_rad = ui.input(|i| i.aim_radius() as f64);
|
||||
|
||||
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 auto_decimals = auto_decimals.clamp(min_decimals, max_decimals);
|
||||
|
||||
let change = ui.input_mut(|input| {
|
||||
let mut change = 0.0;
|
||||
|
||||
if is_kb_editing {
|
||||
// This deliberately doesn't listen for left and right arrow keys,
|
||||
// because when editing, these are used to move the caret.
|
||||
// This behavior is consistent with other editable spinner/stepper
|
||||
// implementations, such as Chromium's (for HTML5 number input).
|
||||
// It is also normal for such controls to go directly into edit mode
|
||||
// when they receive keyboard focus, and some screen readers
|
||||
// assume this behavior, so having a separate mode for incrementing
|
||||
// and decrementing, that supports all arrow keys, would be
|
||||
// problematic.
|
||||
change += input.count_and_consume_key(Modifiers::NONE, Key::ArrowUp) as f64
|
||||
- input.count_and_consume_key(Modifiers::NONE, Key::ArrowDown) as f64;
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
{
|
||||
use accesskit::Action;
|
||||
change += input.num_accesskit_action_requests(id, Action::Increment) as f64
|
||||
- input.num_accesskit_action_requests(id, Action::Decrement) as f64;
|
||||
}
|
||||
|
||||
change
|
||||
});
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
{
|
||||
use accesskit::{Action, ActionData};
|
||||
ui.input(|input| {
|
||||
for request in input.accesskit_action_requests(id, Action::SetValue) {
|
||||
if let Some(ActionData::NumericValue(new_value)) = request.data {
|
||||
value = new_value;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if change != 0.0 {
|
||||
value += speed * change;
|
||||
value = emath::round_to_decimals(value, auto_decimals);
|
||||
}
|
||||
|
||||
value = clamp_to_range(value, clamp_range.clone());
|
||||
if old_value != value {
|
||||
set(&mut get_set_value, value);
|
||||
ui.memory_mut(|mem| mem.drag_value.edit_string = None);
|
||||
}
|
||||
|
||||
let value_text = match custom_formatter {
|
||||
Some(custom_formatter) => custom_formatter(value, auto_decimals..=max_decimals),
|
||||
None => {
|
||||
@@ -394,41 +456,35 @@ impl<'a> Widget for DragValue<'a> {
|
||||
}
|
||||
};
|
||||
|
||||
let kb_edit_id = ui.next_auto_id();
|
||||
let is_kb_editing = ui.memory().has_focus(kb_edit_id);
|
||||
let text_style = ui.style().drag_value_text_style.clone();
|
||||
|
||||
// some clones below are redundant if AccessKit is disabled
|
||||
#[allow(clippy::redundant_clone)]
|
||||
let mut response = if is_kb_editing {
|
||||
let button_width = ui.spacing().interact_size.x;
|
||||
let mut value_text = ui
|
||||
.memory()
|
||||
.drag_value
|
||||
.edit_string
|
||||
.take()
|
||||
.unwrap_or(value_text);
|
||||
.memory_mut(|mem| mem.drag_value.edit_string.take())
|
||||
.unwrap_or_else(|| value_text.clone());
|
||||
let response = ui.add(
|
||||
TextEdit::singleline(&mut value_text)
|
||||
.id(kb_edit_id)
|
||||
.id(id)
|
||||
.desired_width(button_width)
|
||||
.font(TextStyle::Monospace),
|
||||
.font(text_style),
|
||||
);
|
||||
let parsed_value = match custom_parser {
|
||||
Some(parser) => parser(&value_text),
|
||||
None => value_text.parse().ok(),
|
||||
};
|
||||
if let Some(parsed_value) = parsed_value {
|
||||
let parsed_value = clamp_to_range(parsed_value, clamp_range);
|
||||
let parsed_value = clamp_to_range(parsed_value, clamp_range.clone());
|
||||
set(&mut get_set_value, parsed_value);
|
||||
}
|
||||
if ui.input().key_pressed(Key::Enter) {
|
||||
ui.memory().surrender_focus(kb_edit_id);
|
||||
ui.memory().drag_value.edit_string = None;
|
||||
} else {
|
||||
ui.memory().drag_value.edit_string = Some(value_text);
|
||||
}
|
||||
ui.memory_mut(|mem| mem.drag_value.edit_string = Some(value_text));
|
||||
response
|
||||
} else {
|
||||
let button = Button::new(
|
||||
RichText::new(format!("{}{}{}", prefix, value_text, suffix)).monospace(),
|
||||
RichText::new(format!("{}{}{}", prefix, value_text.clone(), suffix))
|
||||
.text_style(text_style),
|
||||
)
|
||||
.wrap(false)
|
||||
.sense(Sense::click_and_drag())
|
||||
@@ -447,10 +503,18 @@ impl<'a> Widget for DragValue<'a> {
|
||||
}
|
||||
|
||||
if response.clicked() {
|
||||
ui.memory().request_focus(kb_edit_id);
|
||||
ui.memory().drag_value.edit_string = None; // Filled in next frame
|
||||
ui.memory_mut(|mem| {
|
||||
mem.drag_value.edit_string = None;
|
||||
mem.request_focus(id);
|
||||
});
|
||||
let mut state = TextEdit::load_state(ui.ctx(), id).unwrap_or_default();
|
||||
state.set_ccursor_range(Some(text::CCursorRange::two(
|
||||
epaint::text::cursor::CCursor::default(),
|
||||
epaint::text::cursor::CCursor::new(value_text.chars().count()),
|
||||
)));
|
||||
state.store(ui.ctx(), response.id);
|
||||
} else if response.dragged() {
|
||||
ui.output().cursor_icon = CursorIcon::ResizeHorizontal;
|
||||
ui.ctx().set_cursor_icon(CursorIcon::ResizeHorizontal);
|
||||
|
||||
let mdelta = response.drag_delta();
|
||||
let delta_points = mdelta.x - mdelta.y; // Increase to the right and up
|
||||
@@ -460,11 +524,11 @@ impl<'a> Widget for DragValue<'a> {
|
||||
let delta_value = delta_points as f64 * speed;
|
||||
|
||||
if delta_value != 0.0 {
|
||||
let mut drag_state = std::mem::take(&mut ui.memory().drag_value);
|
||||
let mut drag_state = ui.memory_mut(|mem| std::mem::take(&mut mem.drag_value));
|
||||
|
||||
// Since we round the value being dragged, we need to store the full precision value in memory:
|
||||
let stored_value = (drag_state.last_dragged_id == Some(response.id))
|
||||
.then(|| drag_state.last_dragged_value)
|
||||
.then_some(drag_state.last_dragged_value)
|
||||
.flatten();
|
||||
let stored_value = stored_value.unwrap_or(value);
|
||||
let stored_value = stored_value + delta_value;
|
||||
@@ -476,24 +540,12 @@ impl<'a> Widget for DragValue<'a> {
|
||||
);
|
||||
let rounded_new_value =
|
||||
emath::round_to_decimals(rounded_new_value, auto_decimals);
|
||||
let rounded_new_value = clamp_to_range(rounded_new_value, clamp_range);
|
||||
let rounded_new_value = clamp_to_range(rounded_new_value, clamp_range.clone());
|
||||
set(&mut get_set_value, rounded_new_value);
|
||||
|
||||
drag_state.last_dragged_id = Some(response.id);
|
||||
drag_state.last_dragged_value = Some(stored_value);
|
||||
ui.memory().drag_value = drag_state;
|
||||
}
|
||||
} else if response.has_focus() {
|
||||
let change = ui.input().num_presses(Key::ArrowUp) as f64
|
||||
+ ui.input().num_presses(Key::ArrowRight) as f64
|
||||
- ui.input().num_presses(Key::ArrowDown) as f64
|
||||
- ui.input().num_presses(Key::ArrowLeft) as f64;
|
||||
|
||||
if change != 0.0 {
|
||||
let new_value = value + speed * change;
|
||||
let new_value = emath::round_to_decimals(new_value, auto_decimals);
|
||||
let new_value = clamp_to_range(new_value, clamp_range);
|
||||
set(&mut get_set_value, new_value);
|
||||
ui.memory_mut(|mem| mem.drag_value = drag_state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -503,6 +555,54 @@ impl<'a> Widget for DragValue<'a> {
|
||||
response.changed = get(&mut get_set_value) != old_value;
|
||||
|
||||
response.widget_info(|| WidgetInfo::drag_value(value));
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
ui.ctx().accesskit_node(response.id, |node| {
|
||||
use accesskit::Action;
|
||||
// If either end of the range is unbounded, it's better
|
||||
// to leave the corresponding AccessKit field set to None,
|
||||
// to allow for platform-specific default behavior.
|
||||
if clamp_range.start().is_finite() {
|
||||
node.min_numeric_value = Some(*clamp_range.start());
|
||||
}
|
||||
if clamp_range.end().is_finite() {
|
||||
node.max_numeric_value = Some(*clamp_range.end());
|
||||
}
|
||||
node.numeric_value_step = Some(speed);
|
||||
node.actions |= Action::SetValue;
|
||||
if value < *clamp_range.end() {
|
||||
node.actions |= Action::Increment;
|
||||
}
|
||||
if value > *clamp_range.start() {
|
||||
node.actions |= Action::Decrement;
|
||||
}
|
||||
// The name field is set to the current value by the button,
|
||||
// but we don't want it set that way on this widget type.
|
||||
node.name = None;
|
||||
// Always expose the value as a string. This makes the widget
|
||||
// more stable to accessibility users as it switches
|
||||
// between edit and button modes. This is particularly important
|
||||
// for VoiceOver on macOS; if the value is not exposed as a string
|
||||
// when the widget is in button mode, then VoiceOver speaks
|
||||
// the value (or a percentage if the widget has a clamp range)
|
||||
// when the widget loses focus, overriding the announcement
|
||||
// of the newly focused widget. This is certainly a VoiceOver bug,
|
||||
// but it's good to make our software work as well as possible
|
||||
// with existing assistive technology. However, if the widget
|
||||
// has a prefix and/or suffix, expose those when in button mode,
|
||||
// just as they're exposed on the screen. This triggers the
|
||||
// VoiceOver bug just described, but exposing all information
|
||||
// is more important, and at least we can avoid the bug
|
||||
// for instances of the widget with no prefix or suffix.
|
||||
//
|
||||
// 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);
|
||||
node.value = Some(value_text.into());
|
||||
}
|
||||
});
|
||||
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ impl Widget for Link {
|
||||
response.widget_info(|| WidgetInfo::labeled(WidgetType::Link, text_galley.text()));
|
||||
|
||||
if response.hovered() {
|
||||
ui.ctx().output().cursor_icon = CursorIcon::PointingHand;
|
||||
ui.ctx().set_cursor_icon(CursorIcon::PointingHand);
|
||||
}
|
||||
|
||||
if ui.is_rect_visible(response.rect) {
|
||||
@@ -48,7 +48,7 @@ impl Widget for Link {
|
||||
let underline = if response.hovered() || response.has_focus() {
|
||||
Stroke::new(visuals.fg_stroke.width, color)
|
||||
} else {
|
||||
Stroke::none()
|
||||
Stroke::NONE
|
||||
};
|
||||
|
||||
ui.painter().add(epaint::TextShape {
|
||||
@@ -110,16 +110,20 @@ impl Widget for Hyperlink {
|
||||
|
||||
let response = ui.add(Link::new(text));
|
||||
if response.clicked() {
|
||||
let modifiers = ui.ctx().input().modifiers;
|
||||
ui.ctx().output().open_url = Some(crate::output::OpenUrl {
|
||||
url: url.clone(),
|
||||
new_tab: modifiers.any(),
|
||||
let modifiers = ui.ctx().input(|i| i.modifiers);
|
||||
ui.ctx().output_mut(|o| {
|
||||
o.open_url = Some(crate::output::OpenUrl {
|
||||
url: url.clone(),
|
||||
new_tab: modifiers.any(),
|
||||
});
|
||||
});
|
||||
}
|
||||
if response.middle_clicked() {
|
||||
ui.ctx().output().open_url = Some(crate::output::OpenUrl {
|
||||
url: url.clone(),
|
||||
new_tab: true,
|
||||
ui.ctx().output_mut(|o| {
|
||||
o.open_url = Some(crate::output::OpenUrl {
|
||||
url: url.clone(),
|
||||
new_tab: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
response.on_hover_text(url)
|
||||
|
||||
@@ -16,7 +16,7 @@ use crate::{widget_text::WidgetTextGalley, *};
|
||||
pub struct Label {
|
||||
text: WidgetText,
|
||||
wrap: Option<bool>,
|
||||
sense: Sense,
|
||||
sense: Option<Sense>,
|
||||
}
|
||||
|
||||
impl Label {
|
||||
@@ -24,7 +24,7 @@ impl Label {
|
||||
Self {
|
||||
text: text.into(),
|
||||
wrap: None,
|
||||
sense: Sense::focusable_noninteractive(),
|
||||
sense: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,11 +34,13 @@ impl Label {
|
||||
|
||||
/// If `true`, the text will wrap to stay within the max width of the [`Ui`].
|
||||
///
|
||||
/// By default [`Self::wrap`] will be true in vertical layouts
|
||||
/// By default [`Self::wrap`] will be `true` in vertical layouts
|
||||
/// and horizontal layouts with wrapping,
|
||||
/// and false on non-wrapping horizontal layouts.
|
||||
/// and `false` on non-wrapping horizontal layouts.
|
||||
///
|
||||
/// Note that any `\n` in the text will always produce a new line.
|
||||
///
|
||||
/// You can also use [`crate::Style::wrap`].
|
||||
#[inline]
|
||||
pub fn wrap(mut self, wrap: bool) -> Self {
|
||||
self.wrap = Some(wrap);
|
||||
@@ -60,7 +62,7 @@ impl Label {
|
||||
/// # });
|
||||
/// ```
|
||||
pub fn sense(mut self, sense: Sense) -> Self {
|
||||
self.sense = sense;
|
||||
self.sense = Some(sense);
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -68,9 +70,17 @@ impl Label {
|
||||
impl Label {
|
||||
/// Do layout and position the galley in the ui, without painting it or adding widget info.
|
||||
pub fn layout_in_ui(self, ui: &mut Ui) -> (Pos2, WidgetTextGalley, Response) {
|
||||
let sense = self.sense.unwrap_or_else(|| {
|
||||
// We only want to focus labels if the screen reader is on.
|
||||
if ui.memory(|mem| mem.options.screen_reader) {
|
||||
Sense::focusable_noninteractive()
|
||||
} else {
|
||||
Sense::hover()
|
||||
}
|
||||
});
|
||||
if let WidgetText::Galley(galley) = self.text {
|
||||
// If the user said "use this specific galley", then just use it:
|
||||
let (rect, response) = ui.allocate_exact_size(galley.size(), self.sense);
|
||||
let (rect, response) = ui.allocate_exact_size(galley.size(), sense);
|
||||
let pos = match galley.job.halign {
|
||||
Align::LEFT => rect.left_top(),
|
||||
Align::Center => rect.center_top(),
|
||||
@@ -110,7 +120,7 @@ impl Label {
|
||||
if let Some(first_section) = text_job.job.sections.first_mut() {
|
||||
first_section.leading_space = first_row_indentation;
|
||||
}
|
||||
let text_galley = text_job.into_galley(&*ui.fonts());
|
||||
let text_galley = ui.fonts(|f| text_job.into_galley(f));
|
||||
|
||||
let pos = pos2(ui.max_rect().left(), ui.cursor().top());
|
||||
assert!(
|
||||
@@ -121,10 +131,10 @@ impl Label {
|
||||
let rect = text_galley.galley.rows[0]
|
||||
.rect
|
||||
.translate(vec2(pos.x, pos.y));
|
||||
let mut response = ui.allocate_rect(rect, self.sense);
|
||||
let mut response = ui.allocate_rect(rect, sense);
|
||||
for row in text_galley.galley.rows.iter().skip(1) {
|
||||
let rect = row.rect.translate(vec2(pos.x, pos.y));
|
||||
response |= ui.allocate_rect(rect, self.sense);
|
||||
response |= ui.allocate_rect(rect, sense);
|
||||
}
|
||||
(pos, text_galley, response)
|
||||
} else {
|
||||
@@ -143,8 +153,8 @@ impl Label {
|
||||
text_job.job.justify = ui.layout().horizontal_justify();
|
||||
};
|
||||
|
||||
let text_galley = text_job.into_galley(&*ui.fonts());
|
||||
let (rect, response) = ui.allocate_exact_size(text_galley.size(), self.sense);
|
||||
let text_galley = ui.fonts(|f| text_job.into_galley(f));
|
||||
let (rect, response) = ui.allocate_exact_size(text_galley.size(), sense);
|
||||
let pos = match text_galley.galley.job.halign {
|
||||
Align::LEFT => rect.left_top(),
|
||||
Align::Center => rect.center_top(),
|
||||
@@ -163,10 +173,10 @@ impl Widget for Label {
|
||||
if ui.is_rect_visible(response.rect) {
|
||||
let response_color = ui.style().interact(&response).text_color();
|
||||
|
||||
let underline = if response.has_focus() {
|
||||
let underline = if response.has_focus() || response.highlighted() {
|
||||
Stroke::new(1.0, response_color)
|
||||
} else {
|
||||
Stroke::none()
|
||||
Stroke::NONE
|
||||
};
|
||||
|
||||
let override_text_color = if text_galley.galley_has_color {
|
||||
|
||||
@@ -185,7 +185,11 @@ impl RectElement for Bar {
|
||||
|
||||
fn default_values_format(&self, transform: &ScreenTransform) -> String {
|
||||
let scale = transform.dvalue_dpos();
|
||||
let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
|
||||
format!("\n{:.*}", y_decimals, self.value)
|
||||
let scale = match self.orientation {
|
||||
Orientation::Horizontal => scale[0],
|
||||
Orientation::Vertical => scale[1],
|
||||
};
|
||||
let decimals = ((-scale.abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
|
||||
crate::plot::format_number(self.value, decimals)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,9 +269,15 @@ impl RectElement for BoxElem {
|
||||
|
||||
fn default_values_format(&self, transform: &ScreenTransform) -> String {
|
||||
let scale = transform.dvalue_dpos();
|
||||
let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
|
||||
let scale = match self.orientation {
|
||||
Orientation::Horizontal => scale[0],
|
||||
Orientation::Vertical => scale[1],
|
||||
};
|
||||
let y_decimals = ((-scale.abs().log10()).ceil().at_least(0.0) as usize)
|
||||
.at_most(6)
|
||||
.at_least(1);
|
||||
format!(
|
||||
"\nMax = {max:.decimals$}\
|
||||
"Max = {max:.decimals$}\
|
||||
\nQuartile 3 = {q3:.decimals$}\
|
||||
\nMedian = {med:.decimals$}\
|
||||
\nQuartile 1 = {q1:.decimals$}\
|
||||
|
||||
@@ -34,6 +34,7 @@ pub(super) struct PlotConfig<'a> {
|
||||
pub(super) trait PlotItem {
|
||||
fn shapes(&self, ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>);
|
||||
|
||||
/// For plot-items which are generated based on x values (plotting functions).
|
||||
fn initialize(&mut self, x_range: RangeInclusive<f64>);
|
||||
|
||||
fn name(&self) -> &str;
|
||||
@@ -176,7 +177,7 @@ impl HLine {
|
||||
}
|
||||
|
||||
impl PlotItem for HLine {
|
||||
fn shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
|
||||
fn shapes(&self, ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
|
||||
let HLine {
|
||||
y,
|
||||
stroke,
|
||||
@@ -184,9 +185,15 @@ impl PlotItem for HLine {
|
||||
style,
|
||||
..
|
||||
} = self;
|
||||
|
||||
// Round to minimize aliasing:
|
||||
let points = vec![
|
||||
transform.position_from_point(&PlotPoint::new(transform.bounds().min[0], *y)),
|
||||
transform.position_from_point(&PlotPoint::new(transform.bounds().max[0], *y)),
|
||||
ui.ctx().round_pos_to_pixels(
|
||||
transform.position_from_point(&PlotPoint::new(transform.bounds().min[0], *y)),
|
||||
),
|
||||
ui.ctx().round_pos_to_pixels(
|
||||
transform.position_from_point(&PlotPoint::new(transform.bounds().max[0], *y)),
|
||||
),
|
||||
];
|
||||
style.style_line(points, *stroke, *highlight, shapes);
|
||||
}
|
||||
@@ -286,7 +293,7 @@ impl VLine {
|
||||
}
|
||||
|
||||
impl PlotItem for VLine {
|
||||
fn shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
|
||||
fn shapes(&self, ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
|
||||
let VLine {
|
||||
x,
|
||||
stroke,
|
||||
@@ -294,9 +301,15 @@ impl PlotItem for VLine {
|
||||
style,
|
||||
..
|
||||
} = self;
|
||||
|
||||
// Round to minimize aliasing:
|
||||
let points = vec![
|
||||
transform.position_from_point(&PlotPoint::new(*x, transform.bounds().min[1])),
|
||||
transform.position_from_point(&PlotPoint::new(*x, transform.bounds().max[1])),
|
||||
ui.ctx().round_pos_to_pixels(
|
||||
transform.position_from_point(&PlotPoint::new(*x, transform.bounds().min[1])),
|
||||
),
|
||||
ui.ctx().round_pos_to_pixels(
|
||||
transform.position_from_point(&PlotPoint::new(*x, transform.bounds().max[1])),
|
||||
),
|
||||
];
|
||||
style.style_line(points, *stroke, *highlight, shapes);
|
||||
}
|
||||
@@ -406,7 +419,7 @@ impl Line {
|
||||
/// a horizontal line at the given y-coordinate.
|
||||
fn y_intersection(p1: &Pos2, p2: &Pos2, y: f32) -> Option<f32> {
|
||||
((p1.y > y && p2.y < y) || (p1.y < y && p2.y > y))
|
||||
.then(|| ((y * (p1.x - p2.x)) - (p1.x * p2.y - p1.y * p2.x)) / (p1.y - p2.y))
|
||||
.then_some(((y * (p1.x - p2.x)) - (p1.x * p2.y - p1.y * p2.x)) / (p1.y - p2.y))
|
||||
}
|
||||
|
||||
impl PlotItem for Line {
|
||||
@@ -593,7 +606,7 @@ impl PlotItem for Polygon {
|
||||
|
||||
let fill = Rgba::from(stroke.color).to_opaque().multiply(fill_alpha);
|
||||
|
||||
let shape = Shape::convex_polygon(values_tf.clone(), fill, Stroke::none());
|
||||
let shape = Shape::convex_polygon(values_tf.clone(), fill, Stroke::NONE);
|
||||
shapes.push(shape);
|
||||
values_tf.push(*values_tf.first().unwrap());
|
||||
style.style_line(values_tf, *stroke, *highlight, shapes);
|
||||
@@ -843,10 +856,11 @@ impl PlotItem for Points {
|
||||
|
||||
let default_stroke = Stroke::new(stroke_size, *color);
|
||||
let mut stem_stroke = default_stroke;
|
||||
let stroke = (!filled)
|
||||
.then(|| default_stroke)
|
||||
.unwrap_or_else(Stroke::none);
|
||||
let fill = filled.then(|| *color).unwrap_or_default();
|
||||
let (fill, stroke) = if *filled {
|
||||
(*color, Stroke::NONE)
|
||||
} else {
|
||||
(Color32::TRANSPARENT, default_stroke)
|
||||
};
|
||||
|
||||
if *highlight {
|
||||
radius *= 2f32.sqrt();
|
||||
@@ -1634,6 +1648,7 @@ fn add_rulers_and_text(
|
||||
let mut text = elem.name().to_owned(); // could be empty
|
||||
|
||||
if show_values {
|
||||
text.push('\n');
|
||||
text.push_str(&elem.default_values_format(plot.transform));
|
||||
}
|
||||
|
||||
@@ -1643,14 +1658,16 @@ fn add_rulers_and_text(
|
||||
let font_id = TextStyle::Body.resolve(plot.ui.style());
|
||||
|
||||
let corner_value = elem.corner_value();
|
||||
shapes.push(Shape::text(
|
||||
&*plot.ui.fonts(),
|
||||
plot.transform.position_from_point(&corner_value) + vec2(3.0, -2.0),
|
||||
Align2::LEFT_BOTTOM,
|
||||
text,
|
||||
font_id,
|
||||
plot.ui.visuals().text_color(),
|
||||
));
|
||||
plot.ui.fonts(|f| {
|
||||
shapes.push(Shape::text(
|
||||
f,
|
||||
plot.transform.position_from_point(&corner_value) + vec2(3.0, -2.0),
|
||||
Align2::LEFT_BOTTOM,
|
||||
text,
|
||||
font_id,
|
||||
plot.ui.visuals().text_color(),
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
/// Draws a cross of horizontal and vertical ruler at the `pointer` position.
|
||||
@@ -1680,8 +1697,8 @@ pub(super) fn rulers_at_value(
|
||||
|
||||
let text = {
|
||||
let scale = plot.transform.dvalue_dpos();
|
||||
let x_decimals = ((-scale[0].abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
|
||||
let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
|
||||
let x_decimals = ((-scale[0].abs().log10()).ceil().at_least(0.0) as usize).clamp(1, 6);
|
||||
let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).clamp(1, 6);
|
||||
if let Some(custom_label) = label_formatter {
|
||||
custom_label(name, &value)
|
||||
} else if plot.show_x && plot.show_y {
|
||||
@@ -1699,15 +1716,16 @@ pub(super) fn rulers_at_value(
|
||||
};
|
||||
|
||||
let font_id = TextStyle::Body.resolve(plot.ui.style());
|
||||
|
||||
shapes.push(Shape::text(
|
||||
&*plot.ui.fonts(),
|
||||
pointer + vec2(3.0, -2.0),
|
||||
Align2::LEFT_BOTTOM,
|
||||
text,
|
||||
font_id,
|
||||
plot.ui.visuals().text_color(),
|
||||
));
|
||||
plot.ui.fonts(|f| {
|
||||
shapes.push(Shape::text(
|
||||
f,
|
||||
pointer + vec2(3.0, -2.0),
|
||||
Align2::LEFT_BOTTOM,
|
||||
text,
|
||||
font_id,
|
||||
plot.ui.visuals().text_color(),
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
fn find_closest_rect<'a, T>(
|
||||
|
||||
@@ -299,7 +299,7 @@ impl PlotPoints {
|
||||
) -> Option<RangeInclusive<f64>> {
|
||||
let start = range1.start().max(*range2.start());
|
||||
let end = range1.end().min(*range2.end());
|
||||
(start < end).then(|| start..=end)
|
||||
(start < end).then_some(start..=end)
|
||||
}
|
||||
|
||||
pub(super) fn bounds(&self) -> PlotBounds {
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::*;
|
||||
use super::items::PlotItem;
|
||||
|
||||
/// Where to place the plot legend.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Corner {
|
||||
LeftTop,
|
||||
RightTop,
|
||||
@@ -89,9 +89,7 @@ impl LegendEntry {
|
||||
|
||||
let font_id = text_style.resolve(ui.style());
|
||||
|
||||
let galley = ui
|
||||
.fonts()
|
||||
.layout_delayed_color(text, font_id, f32::INFINITY);
|
||||
let galley = ui.fonts(|f| f.layout_delayed_color(text, font_id, f32::INFINITY));
|
||||
|
||||
let icon_size = galley.size().y;
|
||||
let icon_spacing = icon_size / 5.0;
|
||||
@@ -189,7 +187,7 @@ impl LegendWidget {
|
||||
LegendEntry::new(color, checked)
|
||||
});
|
||||
});
|
||||
(!entries.is_empty()).then(|| Self {
|
||||
(!entries.is_empty()).then_some(Self {
|
||||
rect,
|
||||
entries,
|
||||
config,
|
||||
@@ -239,7 +237,7 @@ impl Widget for &mut LegendWidget {
|
||||
let background_frame = Frame {
|
||||
inner_margin: vec2(8.0, 4.0).into(),
|
||||
rounding: ui.style().visuals.window_rounding,
|
||||
shadow: epaint::Shadow::default(),
|
||||
shadow: epaint::Shadow::NONE,
|
||||
fill: ui.style().visuals.extreme_bg_color,
|
||||
stroke: ui.style().visuals.window_stroke(),
|
||||
..Default::default()
|
||||
|
||||
@@ -7,8 +7,8 @@ use std::{
|
||||
};
|
||||
|
||||
use crate::*;
|
||||
use epaint::color::Hsva;
|
||||
use epaint::util::FloatOrd;
|
||||
use epaint::Hsva;
|
||||
|
||||
use items::PlotItem;
|
||||
use legend::LegendWidget;
|
||||
@@ -108,11 +108,11 @@ struct PlotMemory {
|
||||
|
||||
impl PlotMemory {
|
||||
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
|
||||
ctx.data().get_persisted(id)
|
||||
ctx.data_mut(|d| d.get_persisted(id))
|
||||
}
|
||||
|
||||
pub fn store(self, ctx: &Context, id: Id) {
|
||||
ctx.data().insert_persisted(id, self);
|
||||
ctx.data_mut(|d| d.insert_persisted(id, self));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,7 +291,10 @@ pub struct Plot {
|
||||
legend_config: Option<Legend>,
|
||||
show_background: bool,
|
||||
show_axes: [bool; 2],
|
||||
|
||||
grid_spacers: [GridSpacer; 2],
|
||||
sharp_grid_lines: bool,
|
||||
clamp_grid: bool,
|
||||
}
|
||||
|
||||
impl Plot {
|
||||
@@ -330,7 +333,10 @@ impl Plot {
|
||||
legend_config: None,
|
||||
show_background: true,
|
||||
show_axes: [true; 2],
|
||||
|
||||
grid_spacers: [log_grid_spacer(10), log_grid_spacer(10)],
|
||||
sharp_grid_lines: true,
|
||||
clamp_grid: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -555,6 +561,14 @@ impl Plot {
|
||||
self
|
||||
}
|
||||
|
||||
/// Clamp the grid to only be visible at the range of data where we have values.
|
||||
///
|
||||
/// Default: `false`.
|
||||
pub fn clamp_grid(mut self, clamp_grid: bool) -> Self {
|
||||
self.clamp_grid = clamp_grid;
|
||||
self
|
||||
}
|
||||
|
||||
/// Expand bounds to include the given x value.
|
||||
/// For instance, to always show the y axis, call `plot.include_x(0.0)`.
|
||||
pub fn include_x(mut self, x: impl Into<f64>) -> Self {
|
||||
@@ -617,6 +631,13 @@ impl Plot {
|
||||
self
|
||||
}
|
||||
|
||||
/// Round grid positions to full pixels to avoid aliasing. Improves plot appearance but might have an
|
||||
/// undesired effect when shifting the plot bounds. Enabled by default.
|
||||
pub fn sharp_grid_lines(mut self, enabled: bool) -> Self {
|
||||
self.sharp_grid_lines = enabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Resets the plot.
|
||||
pub fn reset(mut self) -> Self {
|
||||
self.reset = true;
|
||||
@@ -662,7 +683,10 @@ impl Plot {
|
||||
show_axes,
|
||||
linked_axes,
|
||||
linked_cursors,
|
||||
|
||||
clamp_grid,
|
||||
grid_spacers,
|
||||
sharp_grid_lines,
|
||||
} = self;
|
||||
|
||||
// Determine the size of the plot in the UI
|
||||
@@ -929,9 +953,9 @@ impl Plot {
|
||||
if let Some(hover_pos) = response.hover_pos() {
|
||||
if allow_zoom {
|
||||
let zoom_factor = if data_aspect.is_some() {
|
||||
Vec2::splat(ui.input().zoom_delta())
|
||||
Vec2::splat(ui.input(|i| i.zoom_delta()))
|
||||
} else {
|
||||
ui.input().zoom_delta_2d()
|
||||
ui.input(|i| i.zoom_delta_2d())
|
||||
};
|
||||
if zoom_factor != Vec2::splat(1.0) {
|
||||
transform.zoom(zoom_factor, hover_pos);
|
||||
@@ -939,7 +963,7 @@ impl Plot {
|
||||
}
|
||||
}
|
||||
if allow_scroll {
|
||||
let scroll_delta = ui.input().scroll_delta;
|
||||
let scroll_delta = ui.input(|i| i.scroll_delta);
|
||||
if scroll_delta != Vec2::ZERO {
|
||||
transform.translate_bounds(-scroll_delta);
|
||||
bounds_modified = true.into();
|
||||
@@ -961,10 +985,12 @@ impl Plot {
|
||||
axis_formatters,
|
||||
show_axes,
|
||||
transform: transform.clone(),
|
||||
grid_spacers,
|
||||
draw_cursor_x: linked_cursors.as_ref().map_or(false, |group| group.link_x),
|
||||
draw_cursor_y: linked_cursors.as_ref().map_or(false, |group| group.link_y),
|
||||
draw_cursors,
|
||||
grid_spacers,
|
||||
sharp_grid_lines,
|
||||
clamp_grid,
|
||||
};
|
||||
let plot_cursors = prepared.ui(ui, &response);
|
||||
|
||||
@@ -1040,6 +1066,11 @@ impl PlotUi {
|
||||
*self.last_screen_transform.bounds()
|
||||
}
|
||||
|
||||
/// Set the plot bounds. Can be useful for implementing alternative plot navigation methods.
|
||||
pub fn set_plot_bounds(&mut self, plot_bounds: PlotBounds) {
|
||||
self.last_screen_transform.set_bounds(plot_bounds);
|
||||
}
|
||||
|
||||
/// Move the plot bounds. Can be useful for implementing alternative plot navigation methods.
|
||||
pub fn translate_bounds(&mut self, delta_pos: Vec2) {
|
||||
self.last_screen_transform.translate_bounds(delta_pos);
|
||||
@@ -1055,10 +1086,15 @@ impl PlotUi {
|
||||
self.response.clicked()
|
||||
}
|
||||
|
||||
/// Returns `true` if the plot was clicked by the secondary button.
|
||||
pub fn plot_secondary_clicked(&self) -> bool {
|
||||
self.response.secondary_clicked()
|
||||
}
|
||||
|
||||
/// The pointer position in plot coordinates. Independent of whether the pointer is in the plot area.
|
||||
pub fn pointer_coordinate(&self) -> Option<PlotPoint> {
|
||||
// We need to subtract the drag delta to keep in sync with the frame-delayed screen transform:
|
||||
let last_pos = self.ctx().input().pointer.latest_pos()? - self.response.drag_delta();
|
||||
let last_pos = self.ctx().input(|i| i.pointer.latest_pos())? - self.response.drag_delta();
|
||||
let value = self.plot_from_screen(last_pos);
|
||||
Some(value)
|
||||
}
|
||||
@@ -1276,22 +1312,36 @@ struct PreparedPlot {
|
||||
axis_formatters: [AxisFormatter; 2],
|
||||
show_axes: [bool; 2],
|
||||
transform: ScreenTransform,
|
||||
grid_spacers: [GridSpacer; 2],
|
||||
draw_cursor_x: bool,
|
||||
draw_cursor_y: bool,
|
||||
draw_cursors: Vec<Cursor>,
|
||||
|
||||
grid_spacers: [GridSpacer; 2],
|
||||
sharp_grid_lines: bool,
|
||||
clamp_grid: bool,
|
||||
}
|
||||
|
||||
impl PreparedPlot {
|
||||
fn ui(self, ui: &mut Ui, response: &Response) -> Vec<Cursor> {
|
||||
let mut shapes = Vec::new();
|
||||
let mut axes_shapes = Vec::new();
|
||||
|
||||
for d in 0..2 {
|
||||
if self.show_axes[d] {
|
||||
self.paint_axis(ui, d, &mut shapes);
|
||||
self.paint_axis(
|
||||
ui,
|
||||
d,
|
||||
self.show_axes[1 - d],
|
||||
&mut axes_shapes,
|
||||
self.sharp_grid_lines,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the axes by strength so that those with higher strength are drawn in front.
|
||||
axes_shapes.sort_by(|(_, strength1), (_, strength2)| strength1.total_cmp(strength2));
|
||||
|
||||
let mut shapes = axes_shapes.into_iter().map(|(shape, _)| shape).collect();
|
||||
|
||||
let transform = &self.transform;
|
||||
|
||||
let mut plot_ui = ui.child_ui(*transform.frame(), Layout::default());
|
||||
@@ -1359,11 +1409,21 @@ impl PreparedPlot {
|
||||
cursors
|
||||
}
|
||||
|
||||
fn paint_axis(&self, ui: &Ui, axis: usize, shapes: &mut Vec<Shape>) {
|
||||
fn paint_axis(
|
||||
&self,
|
||||
ui: &Ui,
|
||||
axis: usize,
|
||||
other_axis_shown: bool,
|
||||
shapes: &mut Vec<(Shape, f32)>,
|
||||
sharp_grid_lines: bool,
|
||||
) {
|
||||
#![allow(clippy::collapsible_else_if)]
|
||||
|
||||
let Self {
|
||||
transform,
|
||||
axis_formatters,
|
||||
grid_spacers,
|
||||
clamp_grid,
|
||||
..
|
||||
} = self;
|
||||
|
||||
@@ -1377,7 +1437,6 @@ impl PreparedPlot {
|
||||
let font_id = TextStyle::Body.resolve(ui.style());
|
||||
|
||||
// Where on the cross-dimension to show the label values
|
||||
let bounds = transform.bounds();
|
||||
let value_cross = 0.0_f64.clamp(bounds.min[1 - axis], bounds.max[1 - axis]);
|
||||
|
||||
let input = GridInput {
|
||||
@@ -1386,9 +1445,31 @@ impl PreparedPlot {
|
||||
};
|
||||
let steps = (grid_spacers[axis])(input);
|
||||
|
||||
let clamp_range = clamp_grid.then(|| {
|
||||
let mut tight_bounds = PlotBounds::NOTHING;
|
||||
for item in &self.items {
|
||||
let item_bounds = item.bounds();
|
||||
tight_bounds.merge_x(&item_bounds);
|
||||
tight_bounds.merge_y(&item_bounds);
|
||||
}
|
||||
tight_bounds
|
||||
});
|
||||
|
||||
for step in steps {
|
||||
let value_main = step.value;
|
||||
|
||||
if let Some(clamp_range) = clamp_range {
|
||||
if axis == 0 {
|
||||
if !clamp_range.range_x().contains(&value_main) {
|
||||
continue;
|
||||
};
|
||||
} else {
|
||||
if !clamp_range.range_y().contains(&value_main) {
|
||||
continue;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let value = if axis == 0 {
|
||||
PlotPoint::new(value_main, value_cross)
|
||||
} else {
|
||||
@@ -1398,26 +1479,47 @@ impl PreparedPlot {
|
||||
let pos_in_gui = transform.position_from_point(&value);
|
||||
let spacing_in_points = (transform.dpos_dvalue()[axis] * step.step_size).abs() as f32;
|
||||
|
||||
let line_alpha = remap_clamp(
|
||||
spacing_in_points,
|
||||
(MIN_LINE_SPACING_IN_POINTS as f32)..=300.0,
|
||||
0.0..=0.15,
|
||||
);
|
||||
if spacing_in_points > MIN_LINE_SPACING_IN_POINTS as f32 {
|
||||
let line_strength = remap_clamp(
|
||||
spacing_in_points,
|
||||
MIN_LINE_SPACING_IN_POINTS as f32..=300.0,
|
||||
0.0..=1.0,
|
||||
);
|
||||
|
||||
if line_alpha > 0.0 {
|
||||
let line_color = color_from_alpha(ui, line_alpha);
|
||||
let line_color = color_from_contrast(ui, line_strength);
|
||||
|
||||
let mut p0 = pos_in_gui;
|
||||
let mut p1 = pos_in_gui;
|
||||
p0[1 - axis] = transform.frame().min[1 - axis];
|
||||
p1[1 - axis] = transform.frame().max[1 - axis];
|
||||
shapes.push(Shape::line_segment([p0, p1], Stroke::new(1.0, line_color)));
|
||||
|
||||
if let Some(clamp_range) = clamp_range {
|
||||
if axis == 0 {
|
||||
p0.y = transform.position_from_point_y(clamp_range.min[1]);
|
||||
p1.y = transform.position_from_point_y(clamp_range.max[1]);
|
||||
} else {
|
||||
p0.x = transform.position_from_point_x(clamp_range.min[0]);
|
||||
p1.x = transform.position_from_point_x(clamp_range.max[0]);
|
||||
}
|
||||
}
|
||||
|
||||
if sharp_grid_lines {
|
||||
// Round to avoid aliasing
|
||||
p0 = ui.ctx().round_pos_to_pixels(p0);
|
||||
p1 = ui.ctx().round_pos_to_pixels(p1);
|
||||
}
|
||||
|
||||
shapes.push((
|
||||
Shape::line_segment([p0, p1], Stroke::new(1.0, line_color)),
|
||||
line_strength,
|
||||
));
|
||||
}
|
||||
|
||||
let text_alpha = remap_clamp(spacing_in_points, 40.0..=150.0, 0.0..=0.4);
|
||||
|
||||
if text_alpha > 0.0 {
|
||||
let color = color_from_alpha(ui, text_alpha);
|
||||
const MIN_TEXT_SPACING: f32 = 40.0;
|
||||
if spacing_in_points > MIN_TEXT_SPACING {
|
||||
let text_strength =
|
||||
remap_clamp(spacing_in_points, MIN_TEXT_SPACING..=150.0, 0.0..=1.0);
|
||||
let color = color_from_contrast(ui, text_strength);
|
||||
|
||||
let text: String = if let Some(formatter) = axis_formatters[axis].as_deref() {
|
||||
formatter(value_main, &axis_range)
|
||||
@@ -1425,8 +1527,11 @@ impl PreparedPlot {
|
||||
emath::round_to_decimals(value_main, 5).to_string() // hack
|
||||
};
|
||||
|
||||
// Skip origin label for y-axis if x-axis is already showing it (otherwise displayed twice)
|
||||
let skip_origin_y = axis == 1 && other_axis_shown && value_main == 0.0;
|
||||
|
||||
// Custom formatters can return empty string to signal "no label at this resolution"
|
||||
if !text.is_empty() {
|
||||
if !text.is_empty() && !skip_origin_y {
|
||||
let galley = ui.painter().layout_no_wrap(text, font_id.clone(), color);
|
||||
|
||||
let mut text_pos = pos_in_gui + vec2(1.0, -galley.size().y);
|
||||
@@ -1436,17 +1541,20 @@ impl PreparedPlot {
|
||||
.at_most(transform.frame().max[1 - axis] - galley.size()[1 - axis] - 2.0)
|
||||
.at_least(transform.frame().min[1 - axis] + 1.0);
|
||||
|
||||
shapes.push(Shape::galley(text_pos, galley));
|
||||
shapes.push((Shape::galley(text_pos, galley), text_strength));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn color_from_alpha(ui: &Ui, alpha: f32) -> Color32 {
|
||||
if ui.visuals().dark_mode {
|
||||
Rgba::from_white_alpha(alpha).into()
|
||||
} else {
|
||||
Rgba::from_black_alpha((4.0 * alpha).at_most(1.0)).into()
|
||||
}
|
||||
fn color_from_contrast(ui: &Ui, contrast: f32) -> Color32 {
|
||||
let bg = ui.visuals().extreme_bg_color;
|
||||
let fg = ui.visuals().widgets.open.fg_stroke.color;
|
||||
let mix = 0.5 * contrast.sqrt();
|
||||
Color32::from_rgb(
|
||||
lerp((bg.r() as f32)..=(fg.r() as f32), mix) as u8,
|
||||
lerp((bg.g() as f32)..=(fg.g() as f32), mix) as u8,
|
||||
lerp((bg.b() as f32)..=(fg.b() as f32), mix) as u8,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1539,3 +1647,16 @@ fn fill_marks_between(out: &mut Vec<GridMark>, step_size: f64, (min, max): (f64,
|
||||
});
|
||||
out.extend(marks_iter);
|
||||
}
|
||||
|
||||
/// Helper for formatting a number so that we always show at least a few decimals,
|
||||
/// unless it is an integer, in which case we never show any decimals.
|
||||
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)
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,10 @@ impl PlotBounds {
|
||||
max: [-f64::INFINITY; 2],
|
||||
};
|
||||
|
||||
pub fn from_min_max(min: [f64; 2], max: [f64; 2]) -> Self {
|
||||
Self { min, max }
|
||||
}
|
||||
|
||||
pub fn min(&self) -> [f64; 2] {
|
||||
self.min
|
||||
}
|
||||
@@ -220,6 +224,7 @@ impl ScreenTransform {
|
||||
}
|
||||
}
|
||||
|
||||
/// ui-space rectangle.
|
||||
pub fn frame(&self) -> &Rect {
|
||||
&self.frame
|
||||
}
|
||||
@@ -259,18 +264,27 @@ impl ScreenTransform {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn position_from_point(&self, value: &PlotPoint) -> Pos2 {
|
||||
let x = remap(
|
||||
value.x,
|
||||
pub fn position_from_point_x(&self, value: f64) -> f32 {
|
||||
remap(
|
||||
value,
|
||||
self.bounds.min[0]..=self.bounds.max[0],
|
||||
(self.frame.left() as f64)..=(self.frame.right() as f64),
|
||||
);
|
||||
let y = remap(
|
||||
value.y,
|
||||
) as f32
|
||||
}
|
||||
|
||||
pub fn position_from_point_y(&self, value: f64) -> f32 {
|
||||
remap(
|
||||
value,
|
||||
self.bounds.min[1]..=self.bounds.max[1],
|
||||
(self.frame.bottom() as f64)..=(self.frame.top() as f64), // negated y axis!
|
||||
);
|
||||
pos2(x as f32, y as f32)
|
||||
) as f32
|
||||
}
|
||||
|
||||
pub fn position_from_point(&self, value: &PlotPoint) -> Pos2 {
|
||||
pos2(
|
||||
self.position_from_point_x(value.x),
|
||||
self.position_from_point_y(value.y),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn value_from_position(&self, pos: Pos2) -> PlotPoint {
|
||||
|
||||
@@ -13,6 +13,7 @@ pub struct ProgressBar {
|
||||
progress: f32,
|
||||
desired_width: Option<f32>,
|
||||
text: Option<ProgressBarText>,
|
||||
fill: Option<Color32>,
|
||||
animate: bool,
|
||||
}
|
||||
|
||||
@@ -23,6 +24,7 @@ impl ProgressBar {
|
||||
progress: progress.clamp(0.0, 1.0),
|
||||
desired_width: None,
|
||||
text: None,
|
||||
fill: None,
|
||||
animate: false,
|
||||
}
|
||||
}
|
||||
@@ -33,6 +35,12 @@ impl ProgressBar {
|
||||
self
|
||||
}
|
||||
|
||||
/// The fill color of the bar.
|
||||
pub fn fill(mut self, color: Color32) -> Self {
|
||||
self.fill = Some(color);
|
||||
self
|
||||
}
|
||||
|
||||
/// A custom text to display on the progress bar.
|
||||
pub fn text(mut self, text: impl Into<WidgetText>) -> Self {
|
||||
self.text = Some(ProgressBarText::Custom(text.into()));
|
||||
@@ -60,6 +68,7 @@ impl Widget for ProgressBar {
|
||||
progress,
|
||||
desired_width,
|
||||
text,
|
||||
fill,
|
||||
animate,
|
||||
} = self;
|
||||
|
||||
@@ -78,12 +87,8 @@ impl Widget for ProgressBar {
|
||||
|
||||
let visuals = ui.style().visuals.clone();
|
||||
let rounding = outer_rect.height() / 2.0;
|
||||
ui.painter().rect(
|
||||
outer_rect,
|
||||
rounding,
|
||||
visuals.extreme_bg_color,
|
||||
Stroke::none(),
|
||||
);
|
||||
ui.painter()
|
||||
.rect(outer_rect, rounding, visuals.extreme_bg_color, Stroke::NONE);
|
||||
let inner_rect = Rect::from_min_size(
|
||||
outer_rect.min,
|
||||
vec2(
|
||||
@@ -94,7 +99,8 @@ impl Widget for ProgressBar {
|
||||
|
||||
let (dark, bright) = (0.7, 1.0);
|
||||
let color_factor = if animate {
|
||||
lerp(dark..=bright, ui.input().time.cos().abs())
|
||||
let time = ui.input(|i| i.time);
|
||||
lerp(dark..=bright, time.cos().abs())
|
||||
} else {
|
||||
bright
|
||||
};
|
||||
@@ -102,14 +108,17 @@ impl Widget for ProgressBar {
|
||||
ui.painter().rect(
|
||||
inner_rect,
|
||||
rounding,
|
||||
Color32::from(Rgba::from(visuals.selection.bg_fill) * color_factor as f32),
|
||||
Stroke::none(),
|
||||
Color32::from(
|
||||
Rgba::from(fill.unwrap_or(visuals.selection.bg_fill)) * color_factor as f32,
|
||||
),
|
||||
Stroke::NONE,
|
||||
);
|
||||
|
||||
if animate {
|
||||
let n_points = 20;
|
||||
let start_angle = ui.input().time * std::f64::consts::TAU;
|
||||
let end_angle = start_angle + 240f64.to_radians() * ui.input().time.sin();
|
||||
let time = ui.input(|i| i.time);
|
||||
let start_angle = time * std::f64::consts::TAU;
|
||||
let end_angle = start_angle + 240f64.to_radians() * time.sin();
|
||||
let circle_radius = rounding - 2.0;
|
||||
let points: Vec<Pos2> = (0..n_points)
|
||||
.map(|i| {
|
||||
@@ -120,10 +129,8 @@ impl Widget for ProgressBar {
|
||||
+ vec2(-rounding, 0.0)
|
||||
})
|
||||
.collect();
|
||||
ui.painter().add(Shape::line(
|
||||
points,
|
||||
Stroke::new(2.0, visuals.faint_bg_color),
|
||||
));
|
||||
ui.painter()
|
||||
.add(Shape::line(points, Stroke::new(2.0, visuals.text_color())));
|
||||
}
|
||||
|
||||
if let Some(text_kind) = text {
|
||||
|
||||
@@ -61,11 +61,15 @@ impl Widget for SelectableLabel {
|
||||
|
||||
let visuals = ui.style().interact_selectable(&response, selected);
|
||||
|
||||
if selected || response.hovered() || response.has_focus() {
|
||||
if selected || response.hovered() || response.highlighted() || response.has_focus() {
|
||||
let rect = rect.expand(visuals.expansion);
|
||||
|
||||
ui.painter()
|
||||
.rect(rect, visuals.rounding, visuals.bg_fill, visuals.bg_stroke);
|
||||
ui.painter().rect(
|
||||
rect,
|
||||
visuals.rounding,
|
||||
visuals.weak_bg_fill,
|
||||
visuals.bg_stroke,
|
||||
);
|
||||
}
|
||||
|
||||
text.paint_with_visuals(ui.painter(), text_pos, &visuals);
|
||||
|
||||
@@ -14,6 +14,7 @@ use crate::*;
|
||||
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
|
||||
pub struct Separator {
|
||||
spacing: f32,
|
||||
grow: f32,
|
||||
is_horizontal_line: Option<bool>,
|
||||
}
|
||||
|
||||
@@ -21,6 +22,7 @@ impl Default for Separator {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
spacing: 6.0,
|
||||
grow: 0.0,
|
||||
is_horizontal_line: None,
|
||||
}
|
||||
}
|
||||
@@ -28,12 +30,19 @@ impl Default for Separator {
|
||||
|
||||
impl Separator {
|
||||
/// How much space we take up. The line is painted in the middle of this.
|
||||
///
|
||||
/// In a vertical layout, with a horizontal Separator,
|
||||
/// this is the height of the separator widget.
|
||||
///
|
||||
/// In a horizontal layout, with a vertical Separator,
|
||||
/// this is the width of the separator widget.
|
||||
pub fn spacing(mut self, spacing: f32) -> Self {
|
||||
self.spacing = spacing;
|
||||
self
|
||||
}
|
||||
|
||||
/// Explicitly ask for a horizontal line.
|
||||
///
|
||||
/// By default you will get a horizontal line in vertical layouts,
|
||||
/// and a vertical line in horizontal layouts.
|
||||
pub fn horizontal(mut self) -> Self {
|
||||
@@ -42,18 +51,40 @@ impl Separator {
|
||||
}
|
||||
|
||||
/// Explicitly ask for a vertical line.
|
||||
///
|
||||
/// By default you will get a horizontal line in vertical layouts,
|
||||
/// and a vertical line in horizontal layouts.
|
||||
pub fn vertical(mut self) -> Self {
|
||||
self.is_horizontal_line = Some(false);
|
||||
self
|
||||
}
|
||||
|
||||
/// Extend each end of the separator line by this much.
|
||||
///
|
||||
/// The default is to take up the available width/height of the parent.
|
||||
///
|
||||
/// This will make the line extend outside the parent ui.
|
||||
pub fn grow(mut self, extra: f32) -> Self {
|
||||
self.grow += extra;
|
||||
self
|
||||
}
|
||||
|
||||
/// Contract each end of the separator line by this much.
|
||||
///
|
||||
/// The default is to take up the available width/height of the parent.
|
||||
///
|
||||
/// This effectively adds margins to the line.
|
||||
pub fn shrink(mut self, shrink: f32) -> Self {
|
||||
self.grow -= shrink;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Separator {
|
||||
fn ui(self, ui: &mut Ui) -> Response {
|
||||
let Separator {
|
||||
spacing,
|
||||
grow,
|
||||
is_horizontal_line,
|
||||
} = self;
|
||||
|
||||
@@ -72,10 +103,19 @@ impl Widget for Separator {
|
||||
|
||||
if ui.is_rect_visible(response.rect) {
|
||||
let stroke = ui.visuals().widgets.noninteractive.bg_stroke;
|
||||
let painter = ui.painter();
|
||||
if is_horizontal_line {
|
||||
ui.painter().hline(rect.x_range(), rect.center().y, stroke);
|
||||
painter.hline(
|
||||
(rect.left() - grow)..=(rect.right() + grow),
|
||||
painter.round_to_pixel(rect.center().y),
|
||||
stroke,
|
||||
);
|
||||
} else {
|
||||
ui.painter().vline(rect.center().x, rect.y_range(), stroke);
|
||||
painter.vline(
|
||||
painter.round_to_pixel(rect.center().x),
|
||||
(rect.top() - grow)..=(rect.bottom() + grow),
|
||||
stroke,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ pub struct Slider<'a> {
|
||||
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>,
|
||||
custom_formatter: Option<NumFormatter<'a>>,
|
||||
@@ -123,6 +124,7 @@ impl<'a> Slider<'a> {
|
||||
suffix: Default::default(),
|
||||
text: Default::default(),
|
||||
step: None,
|
||||
drag_value_speed: None,
|
||||
min_decimals: 0,
|
||||
max_decimals: None,
|
||||
custom_formatter: None,
|
||||
@@ -212,6 +214,7 @@ impl<'a> Slider<'a> {
|
||||
}
|
||||
|
||||
/// Sets the minimal change of the value.
|
||||
///
|
||||
/// Value `0.0` effectively disables the feature. If the new value is out of range
|
||||
/// and `clamp_to_range` is enabled, you would not have the ability to change the value.
|
||||
///
|
||||
@@ -221,8 +224,22 @@ impl<'a> Slider<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
/// When dragging the value, how fast does it move?
|
||||
///
|
||||
/// Unit: values per point (logical pixel).
|
||||
/// See also [`DragValue::speed`].
|
||||
///
|
||||
/// By default this is the same speed as when dragging the slider,
|
||||
/// but you can change it here to for instance have a much finer control
|
||||
/// by dragging the slider value rather than the slider itself.
|
||||
pub fn drag_value_speed(mut self, drag_value_speed: f64) -> Self {
|
||||
self.drag_value_speed = Some(drag_value_speed);
|
||||
self
|
||||
}
|
||||
|
||||
// TODO(emilk): we should also have a "min precision".
|
||||
/// Set a minimum number of decimals to display.
|
||||
///
|
||||
/// Normally you don't need to pick a precision, as the slider will intelligently pick a precision for you.
|
||||
/// Regardless of precision the slider will use "smart aim" to help the user select nice, round values.
|
||||
pub fn min_decimals(mut self, min_decimals: usize) -> Self {
|
||||
@@ -232,6 +249,7 @@ impl<'a> Slider<'a> {
|
||||
|
||||
// TODO(emilk): we should also have a "max precision".
|
||||
/// Set a maximum number of decimals to display.
|
||||
///
|
||||
/// Values will also be rounded to this number of decimals.
|
||||
/// Normally you don't need to pick a precision, as the slider will intelligently pick a precision for you.
|
||||
/// Regardless of precision the slider will use "smart aim" to help the user select nice, round values.
|
||||
@@ -241,6 +259,7 @@ impl<'a> Slider<'a> {
|
||||
}
|
||||
|
||||
/// Set an exact number of decimals to display.
|
||||
///
|
||||
/// Values will also be rounded to this number of decimals.
|
||||
/// Normally you don't need to pick a precision, as the slider will intelligently pick a precision for you.
|
||||
/// Regardless of precision the slider will use "smart aim" to help the user select nice, round values.
|
||||
@@ -450,7 +469,7 @@ impl<'a> Slider<'a> {
|
||||
/// If you use one of the integer constructors (e.g. `Slider::i32`) this is called for you,
|
||||
/// but if you want to have a slider for picking integer values in an `Slider::f64`, use this.
|
||||
pub fn integer(self) -> Self {
|
||||
self.fixed_decimals(0).smallest_positive(1.0)
|
||||
self.fixed_decimals(0).smallest_positive(1.0).step_by(1.0)
|
||||
}
|
||||
|
||||
fn get_value(&mut self) -> f64 {
|
||||
@@ -510,7 +529,7 @@ impl<'a> Slider<'a> {
|
||||
SliderOrientation::Horizontal => vec2(ui.spacing().slider_width, thickness),
|
||||
SliderOrientation::Vertical => vec2(thickness, ui.spacing().slider_width),
|
||||
};
|
||||
ui.allocate_response(desired_size, Sense::click_and_drag())
|
||||
ui.allocate_response(desired_size, Sense::drag())
|
||||
}
|
||||
|
||||
/// Just the slider, no text
|
||||
@@ -521,7 +540,7 @@ impl<'a> Slider<'a> {
|
||||
if let Some(pointer_position_2d) = response.interact_pointer_pos() {
|
||||
let position = self.pointer_position(pointer_position_2d);
|
||||
let new_value = if self.smart_aim {
|
||||
let aim_radius = ui.input().aim_radius();
|
||||
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()),
|
||||
@@ -532,6 +551,9 @@ impl<'a> Slider<'a> {
|
||||
self.set_value(new_value);
|
||||
}
|
||||
|
||||
let mut decrement = 0usize;
|
||||
let mut increment = 0usize;
|
||||
|
||||
if response.has_focus() {
|
||||
let (dec_key, inc_key) = match self.orientation {
|
||||
SliderOrientation::Horizontal => (Key::ArrowLeft, Key::ArrowRight),
|
||||
@@ -540,33 +562,51 @@ impl<'a> Slider<'a> {
|
||||
SliderOrientation::Vertical => (Key::ArrowUp, Key::ArrowDown),
|
||||
};
|
||||
|
||||
let decrement = ui.input().num_presses(dec_key);
|
||||
let increment = ui.input().num_presses(inc_key);
|
||||
let kb_step = increment as f32 - decrement as f32;
|
||||
ui.input(|input| {
|
||||
decrement += input.num_presses(dec_key);
|
||||
increment += input.num_presses(inc_key);
|
||||
});
|
||||
}
|
||||
|
||||
if kb_step != 0.0 {
|
||||
let prev_value = self.get_value();
|
||||
let prev_position = self.position_from_value(prev_value, position_range.clone());
|
||||
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().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(),
|
||||
),
|
||||
)
|
||||
#[cfg(feature = "accesskit")]
|
||||
{
|
||||
use accesskit::Action;
|
||||
ui.input(|input| {
|
||||
decrement += input.num_accesskit_action_requests(response.id, Action::Decrement);
|
||||
increment += input.num_accesskit_action_requests(response.id, Action::Increment);
|
||||
});
|
||||
}
|
||||
|
||||
let kb_step = increment as f32 - decrement as f32;
|
||||
|
||||
if kb_step != 0.0 {
|
||||
let prev_value = self.get_value();
|
||||
let prev_position = self.position_from_value(prev_value, position_range.clone());
|
||||
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, position_range.clone()),
|
||||
};
|
||||
self.set_value(new_value);
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
{
|
||||
use accesskit::{Action, ActionData};
|
||||
ui.input(|input| {
|
||||
for request in input.accesskit_action_requests(response.id, Action::SetValue) {
|
||||
if let Some(ActionData::NumericValue(new_value)) = request.data {
|
||||
self.set_value(new_value);
|
||||
}
|
||||
_ => self.value_from_position(new_position, position_range.clone()),
|
||||
};
|
||||
self.set_value(new_value);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Paint it:
|
||||
@@ -583,11 +623,7 @@ impl<'a> Slider<'a> {
|
||||
rect: rail_rect,
|
||||
rounding: ui.visuals().widgets.inactive.rounding,
|
||||
fill: ui.visuals().widgets.inactive.bg_fill,
|
||||
// fill: visuals.bg_fill,
|
||||
// fill: ui.visuals().extreme_bg_color,
|
||||
stroke: Default::default(),
|
||||
// stroke: visuals.bg_stroke,
|
||||
// stroke: ui.visuals().widgets.inactive.bg_stroke,
|
||||
});
|
||||
|
||||
let center = self.marker_center(position_1d, &rail_rect);
|
||||
@@ -657,18 +693,21 @@ impl<'a> Slider<'a> {
|
||||
|
||||
fn value_ui(&mut self, ui: &mut Ui, position_range: RangeInclusive<f32>) -> Response {
|
||||
// If [`DragValue`] is controlled from the keyboard and `step` is defined, set speed to `step`
|
||||
let change = {
|
||||
// Hold one lock rather than 4 (see https://github.com/emilk/egui/pull/1380).
|
||||
let input = ui.input();
|
||||
|
||||
let change = ui.input(|input| {
|
||||
input.num_presses(Key::ArrowUp) as i32 + input.num_presses(Key::ArrowRight) as i32
|
||||
- input.num_presses(Key::ArrowDown) as i32
|
||||
- input.num_presses(Key::ArrowLeft) as i32
|
||||
});
|
||||
|
||||
let any_change = change != 0;
|
||||
let speed = if let (Some(step), true) = (self.step, any_change) {
|
||||
// If [`DragValue`] is controlled from the keyboard and `step` is defined, set speed to `step`
|
||||
step
|
||||
} else {
|
||||
self.drag_value_speed
|
||||
.unwrap_or_else(|| self.current_gradient(&position_range))
|
||||
};
|
||||
let speed = match self.step {
|
||||
Some(step) if change != 0 => step,
|
||||
_ => self.current_gradient(&position_range),
|
||||
};
|
||||
|
||||
let mut value = self.get_value();
|
||||
let response = ui.add({
|
||||
let mut dv = DragValue::new(&mut value)
|
||||
@@ -705,13 +744,37 @@ impl<'a> Slider<'a> {
|
||||
}
|
||||
|
||||
fn add_contents(&mut self, ui: &mut Ui) -> Response {
|
||||
let old_value = self.get_value();
|
||||
|
||||
let thickness = ui
|
||||
.text_style_height(&TextStyle::Body)
|
||||
.at_least(ui.spacing().interact_size.y);
|
||||
let mut response = self.allocate_slider_space(ui, thickness);
|
||||
self.slider_ui(ui, &response);
|
||||
|
||||
if self.show_value {
|
||||
let value = self.get_value();
|
||||
response.changed = value != old_value;
|
||||
response.widget_info(|| WidgetInfo::slider(value, self.text.text()));
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
ui.ctx().accesskit_node(response.id, |node| {
|
||||
use accesskit::Action;
|
||||
node.min_numeric_value = Some(*self.range.start());
|
||||
node.max_numeric_value = Some(*self.range.end());
|
||||
node.numeric_value_step = self.step;
|
||||
node.actions |= Action::SetValue;
|
||||
let clamp_range = self.clamp_range();
|
||||
if value < *clamp_range.end() {
|
||||
node.actions |= Action::Increment;
|
||||
}
|
||||
if value > *clamp_range.start() {
|
||||
node.actions |= Action::Decrement;
|
||||
}
|
||||
});
|
||||
|
||||
let slider_response = response.clone();
|
||||
|
||||
let value_response = if self.show_value {
|
||||
let position_range = self.position_range(&response.rect);
|
||||
let value_response = self.value_ui(ui, position_range);
|
||||
if value_response.gained_focus()
|
||||
@@ -723,12 +786,23 @@ impl<'a> Slider<'a> {
|
||||
response = value_response.union(response);
|
||||
} else {
|
||||
// Use the slider id as the id for the whole widget
|
||||
response = response.union(value_response);
|
||||
response = response.union(value_response.clone());
|
||||
}
|
||||
}
|
||||
Some(value_response)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if !self.text.is_empty() {
|
||||
ui.add(Label::new(self.text.clone()).wrap(false));
|
||||
let label_response = ui.add(Label::new(self.text.clone()).wrap(false));
|
||||
// The slider already has an accessibility label via widget info,
|
||||
// but sometimes it's useful for a screen reader to know
|
||||
// that a piece of text is a label for another widget,
|
||||
// e.g. so the text itself can be excluded from navigation.
|
||||
slider_response.labelled_by(label_response.id);
|
||||
if let Some(value_response) = value_response {
|
||||
value_response.labelled_by(label_response.id);
|
||||
}
|
||||
}
|
||||
|
||||
response
|
||||
@@ -737,18 +811,12 @@ impl<'a> Slider<'a> {
|
||||
|
||||
impl<'a> Widget for Slider<'a> {
|
||||
fn ui(mut self, ui: &mut Ui) -> Response {
|
||||
let old_value = self.get_value();
|
||||
|
||||
let inner_response = match self.orientation {
|
||||
SliderOrientation::Horizontal => ui.horizontal(|ui| self.add_contents(ui)),
|
||||
SliderOrientation::Vertical => ui.vertical(|ui| self.add_contents(ui)),
|
||||
};
|
||||
|
||||
let mut response = inner_response.inner | inner_response.response;
|
||||
let value = self.get_value();
|
||||
response.changed = value != old_value;
|
||||
response.widget_info(|| WidgetInfo::slider(value, self.text.text()));
|
||||
response
|
||||
inner_response.inner | inner_response.response
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,8 +48,9 @@ impl Widget for Spinner {
|
||||
|
||||
let radius = (rect.height() / 2.0) - 2.0;
|
||||
let n_points = 20;
|
||||
let start_angle = ui.input().time * std::f64::consts::TAU;
|
||||
let end_angle = start_angle + 240f64.to_radians() * ui.input().time.sin();
|
||||
let time = ui.input(|i| i.time);
|
||||
let start_angle = time * std::f64::consts::TAU;
|
||||
let end_angle = start_angle + 240f64.to_radians() * time.sin();
|
||||
let points: Vec<Pos2> = (0..n_points)
|
||||
.map(|i| {
|
||||
let angle = lerp(start_angle..=end_angle, i as f64 / n_points as f64);
|
||||
|
||||
@@ -19,7 +19,7 @@ use super::{CCursorRange, CursorRange, TextEditOutput, TextEditState};
|
||||
/// if response.changed() {
|
||||
/// // …
|
||||
/// }
|
||||
/// if response.lost_focus() && ui.input().key_pressed(egui::Key::Enter) {
|
||||
/// if response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
||||
/// // …
|
||||
/// }
|
||||
/// # });
|
||||
@@ -206,7 +206,7 @@ impl<'t> TextEdit<'t> {
|
||||
/// let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| {
|
||||
/// let mut layout_job: egui::text::LayoutJob = my_memoized_highlighter(string);
|
||||
/// layout_job.wrap.max_width = wrap_width;
|
||||
/// ui.fonts().layout_job(layout_job)
|
||||
/// ui.fonts(|f| f.layout_job(layout_job))
|
||||
/// };
|
||||
/// ui.add(egui::TextEdit::multiline(&mut my_code).layouter(&mut layouter));
|
||||
/// # });
|
||||
@@ -312,7 +312,7 @@ impl<'t> TextEdit<'t> {
|
||||
output.response |= ui.interact(frame_rect, id, Sense::click());
|
||||
}
|
||||
if output.response.clicked() && !output.response.lost_focus() {
|
||||
ui.memory().request_focus(output.response.id);
|
||||
ui.memory_mut(|mem| mem.request_focus(output.response.id));
|
||||
}
|
||||
|
||||
if frame {
|
||||
@@ -381,7 +381,7 @@ impl<'t> TextEdit<'t> {
|
||||
let prev_text = text.as_str().to_owned();
|
||||
|
||||
let font_id = font_selection.resolve(ui.style());
|
||||
let row_height = ui.fonts().row_height(&font_id);
|
||||
let row_height = ui.fonts(|f| f.row_height(&font_id));
|
||||
const MIN_WIDTH: f32 = 24.0; // Never make a [`TextEdit`] more narrow than this.
|
||||
let available_width = ui.available_width().at_least(MIN_WIDTH);
|
||||
let desired_width = desired_width.unwrap_or_else(|| ui.spacing().text_edit_width);
|
||||
@@ -394,11 +394,12 @@ impl<'t> TextEdit<'t> {
|
||||
let font_id_clone = font_id.clone();
|
||||
let mut default_layouter = move |ui: &Ui, text: &str, wrap_width: f32| {
|
||||
let text = mask_if_password(password, text);
|
||||
ui.fonts().layout_job(if multiline {
|
||||
let layout_job = if multiline {
|
||||
LayoutJob::simple(text, font_id_clone.clone(), text_color, wrap_width)
|
||||
} else {
|
||||
LayoutJob::simple_singleline(text, font_id_clone.clone(), text_color)
|
||||
})
|
||||
};
|
||||
ui.fonts(|f| f.layout_job(layout_job))
|
||||
};
|
||||
|
||||
let layouter = layouter.unwrap_or(&mut default_layouter);
|
||||
@@ -428,8 +429,8 @@ impl<'t> TextEdit<'t> {
|
||||
// dragging select text, or scroll the enclosing [`ScrollArea`] (if any)?
|
||||
// Since currently copying selected text in not supported on `eframe` web,
|
||||
// we prioritize touch-scrolling:
|
||||
let any_touches = ui.input().any_touches(); // separate line to avoid double-locking the same mutex
|
||||
let allow_drag_to_select = !any_touches || ui.memory().has_focus(id);
|
||||
let allow_drag_to_select =
|
||||
ui.input(|i| !i.any_touches()) || ui.memory(|mem| mem.has_focus(id));
|
||||
|
||||
let sense = if interactive {
|
||||
if allow_drag_to_select {
|
||||
@@ -447,7 +448,7 @@ impl<'t> TextEdit<'t> {
|
||||
if interactive {
|
||||
if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() {
|
||||
if response.hovered() && text.is_mutable() {
|
||||
ui.output().mutable_text_under_cursor = true;
|
||||
ui.output_mut(|o| o.mutable_text_under_cursor = true);
|
||||
}
|
||||
|
||||
// TODO(emilk): drag selected text to either move or clone (ctrl on windows, alt on mac)
|
||||
@@ -457,7 +458,7 @@ impl<'t> TextEdit<'t> {
|
||||
|
||||
if ui.visuals().text_cursor_preview
|
||||
&& response.hovered()
|
||||
&& ui.input().pointer.is_moving()
|
||||
&& ui.input(|i| i.pointer.is_moving())
|
||||
{
|
||||
// preview:
|
||||
paint_cursor_end(
|
||||
@@ -487,10 +488,10 @@ impl<'t> TextEdit<'t> {
|
||||
secondary: galley.from_ccursor(ccursor_range.secondary),
|
||||
}));
|
||||
} else if allow_drag_to_select {
|
||||
if response.hovered() && ui.input().pointer.any_pressed() {
|
||||
ui.memory().request_focus(id);
|
||||
if ui.input().modifiers.shift {
|
||||
if let Some(mut cursor_range) = state.cursor_range(&*galley) {
|
||||
if response.hovered() && ui.input(|i| i.pointer.any_pressed()) {
|
||||
ui.memory_mut(|mem| mem.request_focus(id));
|
||||
if ui.input(|i| i.modifiers.shift) {
|
||||
if let Some(mut cursor_range) = state.cursor_range(&galley) {
|
||||
cursor_range.primary = cursor_at_pointer;
|
||||
state.set_cursor_range(Some(cursor_range));
|
||||
} else {
|
||||
@@ -499,10 +500,11 @@ impl<'t> TextEdit<'t> {
|
||||
} else {
|
||||
state.set_cursor_range(Some(CursorRange::one(cursor_at_pointer)));
|
||||
}
|
||||
} else if ui.input().pointer.any_down() && response.is_pointer_button_down_on()
|
||||
} else if ui.input(|i| i.pointer.any_down())
|
||||
&& response.is_pointer_button_down_on()
|
||||
{
|
||||
// drag to select text:
|
||||
if let Some(mut cursor_range) = state.cursor_range(&*galley) {
|
||||
if let Some(mut cursor_range) = state.cursor_range(&galley) {
|
||||
cursor_range.primary = cursor_at_pointer;
|
||||
state.set_cursor_range(Some(cursor_range));
|
||||
}
|
||||
@@ -511,14 +513,14 @@ impl<'t> TextEdit<'t> {
|
||||
}
|
||||
}
|
||||
|
||||
if response.hovered() && interactive {
|
||||
ui.output().cursor_icon = CursorIcon::Text;
|
||||
if interactive && response.hovered() {
|
||||
ui.ctx().set_cursor_icon(CursorIcon::Text);
|
||||
}
|
||||
|
||||
let mut cursor_range = None;
|
||||
let prev_cursor_range = state.cursor_range(&*galley);
|
||||
if ui.memory().has_focus(id) && interactive {
|
||||
ui.memory().lock_focus(id, lock_focus);
|
||||
let prev_cursor_range = state.cursor_range(&galley);
|
||||
if interactive && ui.memory(|mem| mem.has_focus(id)) {
|
||||
ui.memory_mut(|mem| mem.lock_focus(id, lock_focus));
|
||||
|
||||
let default_cursor_range = if cursor_at_end {
|
||||
CursorRange::one(galley.end())
|
||||
@@ -549,7 +551,7 @@ impl<'t> TextEdit<'t> {
|
||||
|
||||
// Visual clipping for singleline text editor with text larger than width
|
||||
if !multiline {
|
||||
let cursor_pos = match (cursor_range, ui.memory().has_focus(id)) {
|
||||
let cursor_pos = match (cursor_range, ui.memory(|mem| mem.has_focus(id))) {
|
||||
(Some(cursor_range), true) => galley.pos_from_cursor(&cursor_range.primary).min.x,
|
||||
_ => 0.0,
|
||||
};
|
||||
@@ -594,8 +596,8 @@ impl<'t> TextEdit<'t> {
|
||||
galley.paint_with_fallback_color(&painter, response.rect.min, hint_text_color);
|
||||
}
|
||||
|
||||
if ui.memory().has_focus(id) {
|
||||
if let Some(cursor_range) = state.cursor_range(&*galley) {
|
||||
if ui.memory(|mem| mem.has_focus(id)) {
|
||||
if let Some(cursor_range) = state.cursor_range(&galley) {
|
||||
// We paint the cursor on top of the text, in case
|
||||
// the text galley has backgrounds (as e.g. `code` snippets in markup do).
|
||||
paint_cursor_selection(ui, &painter, text_draw_pos, &galley, &cursor_range);
|
||||
@@ -621,9 +623,13 @@ impl<'t> TextEdit<'t> {
|
||||
// But `winit` and `egui_web` differs in how to set the
|
||||
// position of IME.
|
||||
if cfg!(target_arch = "wasm32") {
|
||||
ui.ctx().output().text_cursor_pos = Some(cursor_pos.left_top());
|
||||
ui.ctx().output_mut(|o| {
|
||||
o.text_cursor_pos = Some(cursor_pos.left_top());
|
||||
});
|
||||
} else {
|
||||
ui.ctx().output().text_cursor_pos = Some(cursor_pos.left_bottom());
|
||||
ui.ctx().output_mut(|o| {
|
||||
o.text_cursor_pos = Some(cursor_pos.left_bottom());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -648,11 +654,7 @@ impl<'t> TextEdit<'t> {
|
||||
char_range,
|
||||
mask_if_password(password, text.as_str()),
|
||||
);
|
||||
response
|
||||
.ctx
|
||||
.output()
|
||||
.events
|
||||
.push(OutputEvent::TextSelectionChanged(info));
|
||||
response.output_event(OutputEvent::TextSelectionChanged(info));
|
||||
} else {
|
||||
response.widget_info(|| {
|
||||
WidgetInfo::text_edit(
|
||||
@@ -662,6 +664,101 @@ impl<'t> TextEdit<'t> {
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
{
|
||||
let parent_id = ui.ctx().accesskit_node(response.id, |node| {
|
||||
use accesskit::{TextPosition, TextSelection};
|
||||
|
||||
let parent_id = response.id;
|
||||
|
||||
if let Some(cursor_range) = &cursor_range {
|
||||
let anchor = &cursor_range.secondary.rcursor;
|
||||
let focus = &cursor_range.primary.rcursor;
|
||||
node.text_selection = Some(TextSelection {
|
||||
anchor: TextPosition {
|
||||
node: parent_id.with(anchor.row).accesskit_id(),
|
||||
character_index: anchor.column,
|
||||
},
|
||||
focus: TextPosition {
|
||||
node: parent_id.with(focus.row).accesskit_id(),
|
||||
character_index: focus.column,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
node.default_action_verb = Some(accesskit::DefaultActionVerb::Focus);
|
||||
node.multiline = self.multiline;
|
||||
|
||||
parent_id
|
||||
});
|
||||
|
||||
if let Some(parent_id) = parent_id {
|
||||
// drop ctx lock before further processing
|
||||
use accesskit::{Role, TextDirection};
|
||||
|
||||
ui.ctx().with_accessibility_parent(parent_id, || {
|
||||
for (i, row) in galley.rows.iter().enumerate() {
|
||||
let id = parent_id.with(i);
|
||||
ui.ctx().accesskit_node(id, |node| {
|
||||
node.role = Role::InlineTextBox;
|
||||
let rect = row.rect.translate(text_draw_pos.to_vec2());
|
||||
node.bounds = Some(accesskit::kurbo::Rect {
|
||||
x0: rect.min.x.into(),
|
||||
y0: rect.min.y.into(),
|
||||
x1: rect.max.x.into(),
|
||||
y1: rect.max.y.into(),
|
||||
});
|
||||
node.text_direction = Some(TextDirection::LeftToRight);
|
||||
// TODO(mwcampbell): Set more node fields for the row
|
||||
// once AccessKit adapters expose text formatting info.
|
||||
|
||||
let glyph_count = row.glyphs.len();
|
||||
let mut value = String::new();
|
||||
value.reserve(glyph_count);
|
||||
let mut character_lengths = Vec::<u8>::new();
|
||||
character_lengths.reserve(glyph_count);
|
||||
let mut character_positions = Vec::<f32>::new();
|
||||
character_positions.reserve(glyph_count);
|
||||
let mut character_widths = Vec::<f32>::new();
|
||||
character_widths.reserve(glyph_count);
|
||||
let mut word_lengths = Vec::<u8>::new();
|
||||
let mut was_at_word_end = false;
|
||||
let mut last_word_start = 0usize;
|
||||
|
||||
for glyph in &row.glyphs {
|
||||
let is_word_char = is_word_char(glyph.chr);
|
||||
if is_word_char && was_at_word_end {
|
||||
word_lengths
|
||||
.push((character_lengths.len() - last_word_start) as _);
|
||||
last_word_start = character_lengths.len();
|
||||
}
|
||||
was_at_word_end = !is_word_char;
|
||||
let old_len = value.len();
|
||||
value.push(glyph.chr);
|
||||
character_lengths.push((value.len() - old_len) as _);
|
||||
character_positions.push(glyph.pos.x - row.rect.min.x);
|
||||
character_widths.push(glyph.size.x);
|
||||
}
|
||||
|
||||
if row.ends_with_newline {
|
||||
value.push('\n');
|
||||
character_lengths.push(1);
|
||||
character_positions.push(row.rect.max.x - row.rect.min.x);
|
||||
character_widths.push(0.0);
|
||||
}
|
||||
word_lengths.push((character_lengths.len() - last_word_start) as _);
|
||||
|
||||
node.value = Some(value.into());
|
||||
node.character_lengths = character_lengths.into();
|
||||
node.character_positions = Some(character_positions.into());
|
||||
node.character_widths = Some(character_widths.into());
|
||||
node.word_lengths = word_lengths.into();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
TextEditOutput {
|
||||
response,
|
||||
galley,
|
||||
@@ -689,6 +786,28 @@ fn mask_if_password(is_password: bool, text: &str) -> String {
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
fn ccursor_from_accesskit_text_position(
|
||||
id: Id,
|
||||
galley: &Galley,
|
||||
position: &accesskit::TextPosition,
|
||||
) -> Option<CCursor> {
|
||||
let mut total_length = 0usize;
|
||||
for (i, row) in galley.rows.iter().enumerate() {
|
||||
let row_id = id.with(i);
|
||||
if row_id.accesskit_id() == position.node {
|
||||
return Some(CCursor {
|
||||
index: total_length + position.character_index,
|
||||
prefer_next_row: !(position.character_index == row.glyphs.len()
|
||||
&& !row.ends_with_newline
|
||||
&& (i + 1) < galley.rows.len()),
|
||||
});
|
||||
}
|
||||
total_length += row.glyphs.len() + (row.ends_with_newline as usize);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Check for (keyboard) events to edit the cursor and/or text.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn events(
|
||||
@@ -703,24 +822,24 @@ fn events(
|
||||
password: bool,
|
||||
default_cursor_range: CursorRange,
|
||||
) -> (bool, CursorRange) {
|
||||
let mut cursor_range = state.cursor_range(&*galley).unwrap_or(default_cursor_range);
|
||||
let mut cursor_range = state.cursor_range(galley).unwrap_or(default_cursor_range);
|
||||
|
||||
// We feed state to the undoer both before and after handling input
|
||||
// so that the undoer creates automatic saves even when there are no events for a while.
|
||||
state.undoer.lock().feed_state(
|
||||
ui.input().time,
|
||||
ui.input(|i| i.time),
|
||||
&(cursor_range.as_ccursor_range(), text.as_str().to_owned()),
|
||||
);
|
||||
|
||||
let copy_if_not_password = |ui: &Ui, text: String| {
|
||||
if !password {
|
||||
ui.ctx().output().copied_text = text;
|
||||
ui.ctx().output_mut(|o| o.copied_text = text);
|
||||
}
|
||||
};
|
||||
|
||||
let mut any_change = false;
|
||||
|
||||
let events = ui.input().events.clone(); // avoid dead-lock by cloning. TODO(emilk): optimize
|
||||
let events = ui.input(|i| i.events.clone()); // avoid dead-lock by cloning. TODO(emilk): optimize
|
||||
for event in &events {
|
||||
let did_mutate_text = match event {
|
||||
Event::Copy => {
|
||||
@@ -763,8 +882,9 @@ fn events(
|
||||
key: Key::Tab,
|
||||
pressed: true,
|
||||
modifiers,
|
||||
..
|
||||
} => {
|
||||
if multiline && ui.memory().has_lock_focus(id) {
|
||||
if multiline && ui.memory(|mem| mem.has_lock_focus(id)) {
|
||||
let mut ccursor = delete_selected(text, &cursor_range);
|
||||
if modifiers.shift {
|
||||
// TODO(emilk): support removing indentation over a selection?
|
||||
@@ -788,7 +908,7 @@ fn events(
|
||||
// TODO(emilk): if code editor, auto-indent by same leading tabs, + one if the lines end on an opening bracket
|
||||
Some(CCursorRange::one(ccursor))
|
||||
} else {
|
||||
ui.memory().surrender_focus(id); // End input with enter
|
||||
ui.memory_mut(|mem| mem.surrender_focus(id)); // End input with enter
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -796,6 +916,7 @@ fn events(
|
||||
key: Key::Z,
|
||||
pressed: true,
|
||||
modifiers,
|
||||
..
|
||||
} if modifiers.command && !modifiers.shift => {
|
||||
// TODO(emilk): redo
|
||||
if let Some((undo_ccursor_range, undo_txt)) = state
|
||||
@@ -814,6 +935,7 @@ fn events(
|
||||
key,
|
||||
pressed: true,
|
||||
modifiers,
|
||||
..
|
||||
} => on_key_press(&mut cursor_range, text, galley, *key, modifiers),
|
||||
|
||||
Event::CompositionStart => {
|
||||
@@ -849,6 +971,27 @@ fn events(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
Event::AccessKitActionRequest(accesskit::ActionRequest {
|
||||
action: accesskit::Action::SetTextSelection,
|
||||
target,
|
||||
data: Some(accesskit::ActionData::SetTextSelection(selection)),
|
||||
}) => {
|
||||
if id.accesskit_id() == *target {
|
||||
let primary =
|
||||
ccursor_from_accesskit_text_position(id, galley, &selection.focus);
|
||||
let secondary =
|
||||
ccursor_from_accesskit_text_position(id, galley, &selection.anchor);
|
||||
if let (Some(primary), Some(secondary)) = (primary, secondary) {
|
||||
Some(CCursorRange { primary, secondary })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
_ => None,
|
||||
};
|
||||
|
||||
@@ -869,7 +1012,7 @@ fn events(
|
||||
state.set_cursor_range(Some(cursor_range));
|
||||
|
||||
state.undoer.lock().feed_state(
|
||||
ui.input().time,
|
||||
ui.input(|i| i.time),
|
||||
&(cursor_range.as_ccursor_range(), text.as_str().to_owned()),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use epaint::text::cursor::*;
|
||||
|
||||
/// A selected text range (could be a range of length zero).
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct CursorRange {
|
||||
/// When selecting with a mouse, this is where the mouse was released.
|
||||
|
||||
@@ -34,11 +34,11 @@ pub struct TextEditState {
|
||||
|
||||
impl TextEditState {
|
||||
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
|
||||
ctx.data().get_persisted(id)
|
||||
ctx.data_mut(|d| d.get_persisted(id))
|
||||
}
|
||||
|
||||
pub fn store(self, ctx: &Context, id: Id) {
|
||||
ctx.data().insert_persisted(id, self);
|
||||
ctx.data_mut(|d| d.insert_persisted(id, self));
|
||||
}
|
||||
|
||||
/// The the currently selected range of characters.
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
[package]
|
||||
name = "egui_demo_app"
|
||||
version = "0.19.0"
|
||||
version = "0.20.0"
|
||||
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.62"
|
||||
rust-version = "1.65"
|
||||
publish = false
|
||||
default-run = "egui_demo_app"
|
||||
|
||||
@@ -19,18 +19,9 @@ crate-type = ["cdylib", "rlib"]
|
||||
default = ["glow", "persistence"]
|
||||
|
||||
http = ["ehttp", "image", "poll-promise", "egui_extras/image"]
|
||||
persistence = [
|
||||
"eframe/persistence",
|
||||
"egui/persistence",
|
||||
"serde",
|
||||
]
|
||||
screen_reader = ["eframe/screen_reader"] # experimental
|
||||
serde = [
|
||||
"dep:serde",
|
||||
"egui_demo_lib/serde",
|
||||
"egui_extras/serde",
|
||||
"egui/serde",
|
||||
]
|
||||
persistence = ["eframe/persistence", "egui/persistence", "serde"]
|
||||
web_screen_reader = ["eframe/web_screen_reader"] # experimental
|
||||
serde = ["dep:serde", "egui_demo_lib/serde", "egui/serde"]
|
||||
syntax_highlighting = ["egui_demo_lib/syntax_highlighting"]
|
||||
|
||||
glow = ["eframe/glow"]
|
||||
@@ -39,15 +30,19 @@ wgpu = ["eframe/wgpu", "bytemuck"]
|
||||
|
||||
[dependencies]
|
||||
chrono = { version = "0.4", features = ["js-sys", "wasmbind"] }
|
||||
eframe = { version = "0.19.0", path = "../eframe", default-features = false }
|
||||
egui = { version = "0.19.0", path = "../egui", features = ["extra_debug_asserts"] }
|
||||
egui_demo_lib = { version = "0.19.0", path = "../egui_demo_lib", features = ["chrono"] }
|
||||
eframe = { version = "0.20.0", path = "../eframe", default-features = false }
|
||||
egui = { version = "0.20.0", path = "../egui", features = [
|
||||
"extra_debug_asserts",
|
||||
] }
|
||||
egui_demo_lib = { version = "0.20.0", path = "../egui_demo_lib", features = [
|
||||
"chrono",
|
||||
] }
|
||||
tracing = "0.1"
|
||||
|
||||
# Optional dependencies:
|
||||
|
||||
bytemuck = { version = "1.7.1", optional = true }
|
||||
egui_extras = { version = "0.19.0", optional = true, path = "../egui_extras" }
|
||||
egui_extras = { version = "0.20.0", optional = true, path = "../egui_extras" }
|
||||
|
||||
# feature "http":
|
||||
ehttp = { version = "0.2.0", optional = true }
|
||||
@@ -55,7 +50,7 @@ image = { version = "0.24", optional = true, default-features = false, features
|
||||
"jpeg",
|
||||
"png",
|
||||
] }
|
||||
poll-promise = { version = "0.1", optional = true, default-features = false }
|
||||
poll-promise = { version = "0.2", optional = true, default-features = false }
|
||||
|
||||
# feature "persistence":
|
||||
serde = { version = "1", optional = true, features = ["derive"] }
|
||||
|
||||
@@ -146,12 +146,12 @@ impl RotatingTriangle {
|
||||
),
|
||||
);
|
||||
gl.compile_shader(shader);
|
||||
if !gl.get_shader_compile_status(shader) {
|
||||
panic!(
|
||||
"Failed to compile custom_3d_glow: {}",
|
||||
gl.get_shader_info_log(shader)
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
gl.get_shader_compile_status(shader),
|
||||
"Failed to compile custom_3d_glow {shader_type}: {}",
|
||||
gl.get_shader_info_log(shader)
|
||||
);
|
||||
|
||||
gl.attach_shader(program, shader);
|
||||
shader
|
||||
})
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::{num::NonZeroU64, sync::Arc};
|
||||
|
||||
use eframe::{
|
||||
egui_wgpu::wgpu::util::DeviceExt,
|
||||
egui_wgpu::{self, wgpu},
|
||||
wgpu::util::DeviceExt,
|
||||
};
|
||||
|
||||
pub struct Custom3d {
|
||||
|
||||
@@ -35,7 +35,7 @@ impl Default for FractalClock {
|
||||
impl FractalClock {
|
||||
pub fn ui(&mut self, ui: &mut Ui, seconds_since_midnight: Option<f64>) {
|
||||
if !self.paused {
|
||||
self.time = seconds_since_midnight.unwrap_or_else(|| ui.input().time);
|
||||
self.time = seconds_since_midnight.unwrap_or_else(|| ui.input(|i| i.time));
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ impl FractalClock {
|
||||
ui.expand_to_include_rect(painter.clip_rect());
|
||||
|
||||
Frame::popup(ui.style())
|
||||
.stroke(Stroke::none())
|
||||
.stroke(Stroke::NONE)
|
||||
.show(ui, |ui| {
|
||||
ui.set_max_width(270.0);
|
||||
CollapsingHeader::new("Settings")
|
||||
|
||||
@@ -131,7 +131,7 @@ fn ui_url(ui: &mut egui::Ui, frame: &mut eframe::Frame, url: &mut String) -> boo
|
||||
trigger_fetch = true;
|
||||
}
|
||||
if ui.button("Random image").clicked() {
|
||||
let seed = ui.input().time;
|
||||
let seed = ui.input(|i| i.time);
|
||||
let side = 640;
|
||||
*url = format!("https://picsum.photos/seed/{}/{}", seed, side);
|
||||
trigger_fetch = true;
|
||||
@@ -187,7 +187,7 @@ fn ui_resource(ui: &mut egui::Ui, resource: &Resource) {
|
||||
if let Some(text) = &text {
|
||||
let tooltip = "Click to copy the response body";
|
||||
if ui.button("📋").on_hover_text(tooltip).clicked() {
|
||||
ui.output().copied_text = text.clone();
|
||||
ui.output_mut(|o| o.copied_text = text.clone());
|
||||
}
|
||||
ui.separator();
|
||||
}
|
||||
@@ -245,7 +245,7 @@ impl ColoredText {
|
||||
let mut layouter = |ui: &egui::Ui, _string: &str, wrap_width: f32| {
|
||||
let mut layout_job = self.0.clone();
|
||||
layout_job.wrap.max_width = wrap_width;
|
||||
ui.fonts().layout_job(layout_job)
|
||||
ui.fonts(|f| f.layout_job(layout_job))
|
||||
};
|
||||
|
||||
let mut text = self.0.text.as_str();
|
||||
@@ -258,7 +258,7 @@ impl ColoredText {
|
||||
} else {
|
||||
let mut job = self.0.clone();
|
||||
job.wrap.max_width = ui.available_width();
|
||||
let galley = ui.fonts().layout_job(job);
|
||||
let galley = ui.fonts(|f| f.layout_job(job));
|
||||
let (response, painter) = ui.allocate_painter(galley.size(), egui::Sense::hover());
|
||||
painter.add(egui::Shape::galley(response.rect.min, galley));
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ impl Default for BackendPanel {
|
||||
impl BackendPanel {
|
||||
pub fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||
self.frame_history
|
||||
.on_new_frame(ctx.input().time, frame.info().cpu_usage);
|
||||
.on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage);
|
||||
|
||||
match self.run_mode {
|
||||
RunMode::Continuous => {
|
||||
@@ -130,10 +130,12 @@ impl BackendPanel {
|
||||
|
||||
ui.separator();
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[cfg(feature = "web_screen-reader")]
|
||||
{
|
||||
let mut screen_reader = ui.ctx().options().screen_reader;
|
||||
let mut screen_reader = ui.ctx().options(|o| o.screen_reader);
|
||||
ui.checkbox(&mut screen_reader, "🔈 Screen reader").on_hover_text("Experimental feature: checking this will turn on the screen reader on supported platforms");
|
||||
ui.ctx().options().screen_reader = screen_reader;
|
||||
ui.ctx().options_mut(|o| o.screen_reader = screen_reader);
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -172,9 +174,13 @@ impl BackendPanel {
|
||||
ui.horizontal(|ui| {
|
||||
{
|
||||
let mut fullscreen = frame.info().window_info.fullscreen;
|
||||
ui.checkbox(&mut fullscreen, "🗖 Fullscreen")
|
||||
.on_hover_text("Fullscreen the window");
|
||||
frame.set_fullscreen(fullscreen);
|
||||
if ui
|
||||
.checkbox(&mut fullscreen, "🗖 Fullscreen (F11)")
|
||||
.on_hover_text("Fullscreen the window")
|
||||
.changed()
|
||||
{
|
||||
frame.set_fullscreen(fullscreen);
|
||||
}
|
||||
}
|
||||
|
||||
if ui
|
||||
@@ -335,9 +341,11 @@ impl EguiWindows {
|
||||
output_event_history,
|
||||
} = self;
|
||||
|
||||
for event in &ctx.output().events {
|
||||
output_event_history.push_back(event.clone());
|
||||
}
|
||||
ctx.output(|o| {
|
||||
for event in &o.events {
|
||||
output_event_history.push_back(event.clone());
|
||||
}
|
||||
});
|
||||
while output_event_history.len() > 1000 {
|
||||
output_event_history.pop_front();
|
||||
}
|
||||
|
||||
@@ -95,19 +95,21 @@ impl FrameHistory {
|
||||
));
|
||||
let cpu_usage = to_screen.inverse().transform_pos(pointer_pos).y;
|
||||
let text = format!("{:.1} ms", 1e3 * cpu_usage);
|
||||
shapes.push(Shape::text(
|
||||
&*ui.fonts(),
|
||||
pos2(rect.left(), y),
|
||||
egui::Align2::LEFT_BOTTOM,
|
||||
text,
|
||||
TextStyle::Monospace.resolve(ui.style()),
|
||||
color,
|
||||
));
|
||||
shapes.push(ui.fonts(|f| {
|
||||
Shape::text(
|
||||
f,
|
||||
pos2(rect.left(), y),
|
||||
egui::Align2::LEFT_BOTTOM,
|
||||
text,
|
||||
TextStyle::Monospace.resolve(ui.style()),
|
||||
color,
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
let circle_color = color;
|
||||
let radius = 2.0;
|
||||
let right_side_time = ui.input().time; // Time at right side of screen
|
||||
let right_side_time = ui.input(|i| i.time); // Time at right side of screen
|
||||
|
||||
for (time, cpu_usage) in history.iter() {
|
||||
let age = (right_side_time - time) as f32;
|
||||
|
||||
@@ -59,15 +59,13 @@ pub fn init_wasm_hooks() {
|
||||
#[wasm_bindgen]
|
||||
pub async fn start_separate(canvas_id: &str) -> Result<WebHandle, wasm_bindgen::JsValue> {
|
||||
let web_options = eframe::WebOptions::default();
|
||||
let handle = eframe::start_web(
|
||||
eframe::start_web(
|
||||
canvas_id,
|
||||
web_options,
|
||||
Box::new(|cc| Box::new(WrapApp::new(cc))),
|
||||
)
|
||||
.await
|
||||
.map(|handle| WebHandle { handle });
|
||||
|
||||
handle
|
||||
.map(|handle| WebHandle { handle })
|
||||
}
|
||||
|
||||
/// This is the entry-point for all the web-assembly.
|
||||
|
||||
@@ -3,7 +3,18 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
|
||||
|
||||
// When compiling natively:
|
||||
fn main() {
|
||||
fn main() -> Result<(), eframe::Error> {
|
||||
{
|
||||
// Silence wgpu log spam (https://github.com/gfx-rs/wgpu/issues/3206)
|
||||
let mut rust_log = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_owned());
|
||||
for loud_crate in ["naga", "wgpu_core", "wgpu_hal"] {
|
||||
if !rust_log.contains(&format!("{loud_crate}=")) {
|
||||
rust_log += &format!(",{loud_crate}=warn");
|
||||
}
|
||||
}
|
||||
std::env::set_var("RUST_LOG", rust_log);
|
||||
}
|
||||
|
||||
// Log to stdout (if you run with `RUST_LOG=debug`).
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
@@ -21,5 +32,5 @@ fn main() {
|
||||
"egui demo app",
|
||||
options,
|
||||
Box::new(|cc| Box::new(egui_demo_app::WrapApp::new(cc))),
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -175,8 +175,8 @@ impl eframe::App for WrapApp {
|
||||
eframe::set_value(storage, eframe::APP_KEY, &self.state);
|
||||
}
|
||||
|
||||
fn clear_color(&self, visuals: &egui::Visuals) -> egui::Rgba {
|
||||
visuals.window_fill().into()
|
||||
fn clear_color(&self, visuals: &egui::Visuals) -> [f32; 4] {
|
||||
visuals.panel_fill.to_normalized_gamma_f32()
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||
@@ -190,6 +190,11 @@ impl eframe::App for WrapApp {
|
||||
self.state.selected_anchor = selected_anchor;
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if ctx.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::F11)) {
|
||||
frame.set_fullscreen(!frame.info().window_info.fullscreen);
|
||||
}
|
||||
|
||||
egui::TopBottomPanel::top("wrap_app_top_bar").show(ctx, |ui| {
|
||||
egui::trace!(ui);
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
@@ -233,7 +238,8 @@ impl WrapApp {
|
||||
fn backend_panel(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||
// The backend-panel can be toggled on/off.
|
||||
// We show a little animation when the user switches it.
|
||||
let is_open = self.state.backend_panel.open || ctx.memory().everything_is_visible();
|
||||
let is_open =
|
||||
self.state.backend_panel.open || ctx.memory(|mem| mem.everything_is_visible());
|
||||
|
||||
egui::SidePanel::left("backend_panel")
|
||||
.resizable(false)
|
||||
@@ -258,13 +264,13 @@ impl WrapApp {
|
||||
.on_hover_text("Forget scroll, positions, sizes etc")
|
||||
.clicked()
|
||||
{
|
||||
*ui.ctx().memory() = Default::default();
|
||||
ui.ctx().memory_mut(|mem| *mem = Default::default());
|
||||
ui.close_menu();
|
||||
}
|
||||
|
||||
if ui.button("Reset everything").clicked() {
|
||||
self.state = Default::default();
|
||||
*ui.ctx().memory() = Default::default();
|
||||
ui.ctx().memory_mut(|mem| *mem = Default::default());
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
@@ -274,7 +280,7 @@ impl WrapApp {
|
||||
let mut found_anchor = false;
|
||||
let selected_anchor = self.state.selected_anchor.clone();
|
||||
for (_name, anchor, app) in self.apps_iter_mut() {
|
||||
if anchor == selected_anchor || ctx.memory().everything_is_visible() {
|
||||
if anchor == selected_anchor || ctx.memory(|mem| mem.everything_is_visible()) {
|
||||
app.update(ctx, frame);
|
||||
found_anchor = true;
|
||||
}
|
||||
@@ -308,7 +314,7 @@ impl WrapApp {
|
||||
{
|
||||
selected_anchor = anchor.to_owned();
|
||||
if frame.is_web() {
|
||||
ui.output().open_url(format!("#{}", anchor));
|
||||
ui.output_mut(|o| o.open_url(format!("#{}", anchor)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -320,7 +326,7 @@ impl WrapApp {
|
||||
if clock_button(ui, crate::seconds_since_midnight()).clicked() {
|
||||
self.state.selected_anchor = "clock".to_owned();
|
||||
if frame.is_web() {
|
||||
ui.output().open_url("#clock");
|
||||
ui.output_mut(|o| o.open_url("#clock"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -334,22 +340,25 @@ impl WrapApp {
|
||||
use std::fmt::Write as _;
|
||||
|
||||
// Preview hovering files:
|
||||
if !ctx.input().raw.hovered_files.is_empty() {
|
||||
let mut text = "Dropping files:\n".to_owned();
|
||||
for file in &ctx.input().raw.hovered_files {
|
||||
if let Some(path) = &file.path {
|
||||
write!(text, "\n{}", path.display()).ok();
|
||||
} else if !file.mime.is_empty() {
|
||||
write!(text, "\n{}", file.mime).ok();
|
||||
} else {
|
||||
text += "\n???";
|
||||
if !ctx.input(|i| i.raw.hovered_files.is_empty()) {
|
||||
let text = ctx.input(|i| {
|
||||
let mut text = "Dropping files:\n".to_owned();
|
||||
for file in &i.raw.hovered_files {
|
||||
if let Some(path) = &file.path {
|
||||
write!(text, "\n{}", path.display()).ok();
|
||||
} else if !file.mime.is_empty() {
|
||||
write!(text, "\n{}", file.mime).ok();
|
||||
} else {
|
||||
text += "\n???";
|
||||
}
|
||||
}
|
||||
}
|
||||
text
|
||||
});
|
||||
|
||||
let painter =
|
||||
ctx.layer_painter(LayerId::new(Order::Foreground, Id::new("file_drop_target")));
|
||||
|
||||
let screen_rect = ctx.input().screen_rect();
|
||||
let screen_rect = ctx.screen_rect();
|
||||
painter.rect_filled(screen_rect, 0.0, Color32::from_black_alpha(192));
|
||||
painter.text(
|
||||
screen_rect.center(),
|
||||
@@ -361,9 +370,11 @@ impl WrapApp {
|
||||
}
|
||||
|
||||
// Collect dropped files:
|
||||
if !ctx.input().raw.dropped_files.is_empty() {
|
||||
self.dropped_files = ctx.input().raw.dropped_files.clone();
|
||||
}
|
||||
ctx.input(|i| {
|
||||
if !i.raw.dropped_files.is_empty() {
|
||||
self.dropped_files = i.raw.dropped_files.clone();
|
||||
}
|
||||
});
|
||||
|
||||
// Show dropped files (if any):
|
||||
if !self.dropped_files.is_empty() {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
[package]
|
||||
name = "egui_demo_lib"
|
||||
version = "0.19.0"
|
||||
version = "0.20.0"
|
||||
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
|
||||
description = "Example library for egui"
|
||||
edition = "2021"
|
||||
rust-version = "1.62"
|
||||
rust-version = "1.65"
|
||||
homepage = "https://github.com/emilk/egui/tree/master/crates/egui_demo_lib"
|
||||
license = "MIT OR Apache-2.0"
|
||||
readme = "README.md"
|
||||
@@ -30,18 +30,20 @@ syntax_highlighting = ["syntect"]
|
||||
|
||||
|
||||
[dependencies]
|
||||
egui = { version = "0.19.0", path = "../egui", default-features = false }
|
||||
egui_extras = { version = "0.19.0", path = "../egui_extras" }
|
||||
egui = { version = "0.20.0", path = "../egui", default-features = false }
|
||||
egui_extras = { version = "0.20.0", path = "../egui_extras" }
|
||||
enum-map = { version = "2", features = ["serde"] }
|
||||
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
||||
unicode_names2 = { version = "0.5.0", default-features = false }
|
||||
unicode_names2 = { version = "0.6.0", default-features = false }
|
||||
|
||||
#! ### Optional dependencies
|
||||
chrono = { version = "0.4", optional = true, features = ["js-sys", "wasmbind"] }
|
||||
## Enable this when generating docs.
|
||||
document-features = { version = "0.2", optional = true }
|
||||
serde = { version = "1", optional = true, features = ["derive"] }
|
||||
syntect = { version = "5", optional = true, default-features = false, features = ["default-fancy"] }
|
||||
syntect = { version = "5", optional = true, default-features = false, features = [
|
||||
"default-fancy",
|
||||
] }
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -38,7 +38,7 @@ pub fn criterion_benchmark(c: &mut Criterion) {
|
||||
|
||||
if false {
|
||||
let ctx = egui::Context::default();
|
||||
ctx.memory().set_everything_is_visible(true); // give us everything
|
||||
ctx.memory_mut(|m| m.set_everything_is_visible(true)); // give us everything
|
||||
let mut demo_windows = egui_demo_lib::DemoWindows::default();
|
||||
c.bench_function("demo_full_no_tessellate", |b| {
|
||||
b.iter(|| {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use egui::{color::*, widgets::color_picker::show_color, TextureOptions, *};
|
||||
use egui::{widgets::color_picker::show_color, TextureOptions, *};
|
||||
|
||||
const GRADIENT_SIZE: Vec2 = vec2(256.0, 18.0);
|
||||
|
||||
@@ -98,7 +98,6 @@ impl ColorTest {
|
||||
// TODO(emilk): test color multiplication (image tint),
|
||||
// to make sure vertex and texture color multiplication is done in linear space.
|
||||
|
||||
ui.separator();
|
||||
ui.label("Gamma interpolation:");
|
||||
self.show_gradients(ui, WHITE, (RED, GREEN), Interpolation::Gamma);
|
||||
|
||||
@@ -461,21 +460,21 @@ fn paint_fine_lines_and_text(painter: &egui::Painter, mut rect: Rect, color: Col
|
||||
Align2::LEFT_TOP,
|
||||
format!("{:.0}% white", 100.0 * opacity),
|
||||
FontId::proportional(14.0),
|
||||
Color32::WHITE.linear_multiply(opacity),
|
||||
Color32::WHITE.gamma_multiply(opacity),
|
||||
);
|
||||
painter.text(
|
||||
rect.center_top() + vec2(80.0, y),
|
||||
Align2::LEFT_TOP,
|
||||
format!("{:.0}% gray", 100.0 * opacity),
|
||||
FontId::proportional(14.0),
|
||||
Color32::GRAY.linear_multiply(opacity),
|
||||
Color32::GRAY.gamma_multiply(opacity),
|
||||
);
|
||||
painter.text(
|
||||
rect.center_top() + vec2(160.0, y),
|
||||
Align2::LEFT_TOP,
|
||||
format!("{:.0}% black", 100.0 * opacity),
|
||||
FontId::proportional(14.0),
|
||||
Color32::BLACK.linear_multiply(opacity),
|
||||
Color32::BLACK.gamma_multiply(opacity),
|
||||
);
|
||||
y += 20.0;
|
||||
}
|
||||
@@ -496,8 +495,8 @@ fn paint_fine_lines_and_text(painter: &egui::Painter, mut rect: Rect, color: Col
|
||||
|
||||
rect.max.x = rect.center().x;
|
||||
|
||||
rect = rect.shrink(12.0);
|
||||
for width in [0.5, 1.0, 2.0] {
|
||||
rect = rect.shrink(16.0);
|
||||
for width in [0.05, 0.1, 0.25, 0.5, 1.0, 2.0, 4.0] {
|
||||
painter.text(
|
||||
rect.left_top(),
|
||||
Align2::CENTER_CENTER,
|
||||
@@ -518,8 +517,8 @@ fn paint_fine_lines_and_text(painter: &egui::Painter, mut rect: Rect, color: Col
|
||||
Stroke::new(width, color),
|
||||
));
|
||||
|
||||
rect.min.y += 32.0;
|
||||
rect.max.x -= 32.0;
|
||||
rect.min.y += 24.0;
|
||||
rect.max.x -= 24.0;
|
||||
}
|
||||
|
||||
rect.min.y += 16.0;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user