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

Merge branch 'master' of https://github.com/emilk/egui into multiples_viewports

This commit is contained in:
Konkitoman
2023-09-05 12:19:02 +03:00
20 changed files with 614 additions and 190 deletions

View File

@@ -15,7 +15,7 @@ jobs:
name: Format + check + test name: Format + check + test
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master - uses: dtolnay/rust-toolchain@master
with: with:
@@ -84,7 +84,7 @@ jobs:
name: Check wasm32 + wasm-bindgen name: Check wasm32 + wasm-bindgen
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master - uses: dtolnay/rust-toolchain@master
with: with:
toolchain: 1.67.0 toolchain: 1.67.0
@@ -142,7 +142,7 @@ jobs:
name: cargo-deny ${{ matrix.target }} name: cargo-deny ${{ matrix.target }}
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v1 - uses: EmbarkStudios/cargo-deny-action@v1
with: with:
rust-version: "1.67.0" rust-version: "1.67.0"
@@ -156,7 +156,7 @@ jobs:
name: android name: android
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master - uses: dtolnay/rust-toolchain@master
with: with:
@@ -175,7 +175,7 @@ jobs:
name: Check Windows name: Check Windows
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master - uses: dtolnay/rust-toolchain@master
with: with:
toolchain: 1.67.0 toolchain: 1.67.0

View File

@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout Actions Repository - name: Checkout Actions Repository
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Check spelling of entire workspace - name: Check spelling of entire workspace
uses: crate-ci/typos@master uses: crate-ci/typos@master

View File

@@ -762,8 +762,8 @@ impl Frame {
} }
/// Information about the integration. /// Information about the integration.
pub fn info(&self) -> IntegrationInfo { pub fn info(&self) -> &IntegrationInfo {
self.info.clone() &self.info
} }
/// A place where you can store custom data in a way that persists when you restart the app. /// A place where you can store custom data in a way that persists when you restart the app.

View File

@@ -1003,7 +1003,7 @@ mod glow_integration {
let window = &mut *window.write(); let window = &mut *window.write();
app = app_creator(&epi::CreationContext { app = app_creator(&epi::CreationContext {
egui_ctx: integration.egui_ctx.clone(), egui_ctx: integration.egui_ctx.clone(),
integration_info: integration.frame.info(), integration_info: integration.frame.info().clone(),
storage: integration.frame.storage(), storage: integration.frame.storage(),
gl: Some(gl.clone()), gl: Some(gl.clone()),
#[cfg(feature = "wgpu")] #[cfg(feature = "wgpu")]
@@ -2020,7 +2020,7 @@ mod wgpu_integration {
.expect("Single-use AppCreator has unexpectedly already been taken"); .expect("Single-use AppCreator has unexpectedly already been taken");
let mut app = app_creator(&epi::CreationContext { let mut app = app_creator(&epi::CreationContext {
egui_ctx: integration.egui_ctx.clone(), egui_ctx: integration.egui_ctx.clone(),
integration_info: integration.frame.info(), integration_info: integration.frame.info().clone(),
storage: integration.frame.storage(), storage: integration.frame.storage(),
#[cfg(feature = "glow")] #[cfg(feature = "glow")]
gl: None, gl: None,

View File

@@ -54,6 +54,11 @@ impl State {
pub fn store(self, ctx: &Context, id: Id) { pub fn store(self, ctx: &Context, id: Id) {
ctx.data_mut(|d| d.insert_persisted(id, self)); ctx.data_mut(|d| d.insert_persisted(id, self));
} }
/// Get the current kinetic scrolling velocity.
pub fn velocity(&self) -> Vec2 {
self.vel
}
} }
pub struct ScrollAreaOutput<R> { pub struct ScrollAreaOutput<R> {

View File

@@ -92,14 +92,16 @@
//! # }); //! # });
//! ``` //! ```
//! //!
//! ## Conventions //! ## Coordinate system
//! The left-top corner of the screen is `(0.0, 0.0)`,
//! with X increasing to the right and Y increasing downwards.
//! //!
//! Conventions unless otherwise specified: //! `egui` uses logical _points_ as its coordinate system.
//! Those related to physical _pixels_ by the `pixels_per_point` scale factor.
//! For example, a high-dpi screeen can have `pixels_per_point = 2.0`,
//! meaning there are two physical screen pixels for each logical point.
//! //!
//! * angles are in radians //! Angles are in radians, and are measured clockwise from the X-axis, which has angle=0.
//! * `Vec2::X` is right and `Vec2::Y` is down.
//! * `Pos2::ZERO` is left top.
//! * Positions and sizes are measured in _points_. Each point may consist of many physical pixels.
//! //!
//! # Integrating with egui //! # Integrating with egui
//! //!
@@ -353,7 +355,7 @@ pub mod text {
pub use crate::text_edit::CCursorRange; pub use crate::text_edit::CCursorRange;
pub use epaint::text::{ pub use epaint::text::{
cursor::CCursor, FontData, FontDefinitions, FontFamily, Fonts, Galley, LayoutJob, cursor::CCursor, FontData, FontDefinitions, FontFamily, Fonts, Galley, LayoutJob,
LayoutSection, TextFormat, TAB_SIZE, LayoutSection, TextFormat, TextWrapping, TAB_SIZE,
}; };
} }

View File

@@ -3,8 +3,7 @@
// For non-serializable types, these simply return `None`. // For non-serializable types, these simply return `None`.
// This will also allow users to pick their own serialization format per type. // This will also allow users to pick their own serialization format per type.
use std::any::Any; use std::{any::Any, sync::Arc};
use std::sync::Arc;
// ----------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------------
@@ -32,6 +31,8 @@ impl From<std::any::TypeId> for TypeId {
} }
} }
impl nohash_hasher::IsEnabled for TypeId {}
// ----------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------------
#[cfg(feature = "persistence")] #[cfg(feature = "persistence")]
@@ -54,11 +55,21 @@ impl<T> SerializableAny for T where T: 'static + Any + Clone + for<'a> Send + Sy
// ----------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------------
#[cfg(feature = "persistence")]
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Debug)]
struct SerializedElement { struct SerializedElement {
/// The type of value we are storing.
type_id: TypeId, type_id: TypeId,
/// The ron data we can deserialize.
ron: Arc<str>, ron: Arc<str>,
/// Increased by one each time we re-serialize an element that was never deserialized.
///
/// Large value = old value that hasn't been read in a while.
///
/// Used to garbage collect old values that hasn't been read in a while.
generation: usize,
} }
#[cfg(feature = "persistence")] #[cfg(feature = "persistence")]
@@ -80,13 +91,7 @@ enum Element {
}, },
/// A serialized value /// A serialized value
Serialized { Serialized(SerializedElement),
/// The type of value we are storing.
type_id: TypeId,
/// The ron data we can deserialize.
ron: Arc<str>,
},
} }
impl Clone for Element { impl Clone for Element {
@@ -104,10 +109,7 @@ impl Clone for Element {
serialize_fn: *serialize_fn, serialize_fn: *serialize_fn,
}, },
Self::Serialized { type_id, ron } => Self::Serialized { Self::Serialized(element) => Self::Serialized(element.clone()),
type_id: *type_id,
ron: ron.clone(),
},
} }
} }
} }
@@ -116,13 +118,18 @@ impl std::fmt::Debug for Element {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self { match &self {
Self::Value { value, .. } => f Self::Value { value, .. } => f
.debug_struct("MaybeSerializable::Value") .debug_struct("Element::Value")
.field("type_id", &value.type_id()) .field("type_id", &value.type_id())
.finish_non_exhaustive(), .finish_non_exhaustive(),
Self::Serialized { type_id, ron } => f Self::Serialized(SerializedElement {
.debug_struct("MaybeSerializable::Serialized") type_id,
.field("type_id", &type_id) ron,
.field("ron", &ron) generation,
}) => f
.debug_struct("Element::Serialized")
.field("type_id", type_id)
.field("ron", ron)
.field("generation", generation)
.finish(), .finish(),
} }
} }
@@ -165,7 +172,7 @@ impl Element {
pub(crate) fn type_id(&self) -> TypeId { pub(crate) fn type_id(&self) -> TypeId {
match self { match self {
Self::Value { value, .. } => (**value).type_id().into(), Self::Value { value, .. } => (**value).type_id().into(),
Self::Serialized { type_id, .. } => *type_id, Self::Serialized(SerializedElement { type_id, .. }) => *type_id,
} }
} }
@@ -173,7 +180,7 @@ impl Element {
pub(crate) fn get_temp<T: 'static>(&self) -> Option<&T> { pub(crate) fn get_temp<T: 'static>(&self) -> Option<&T> {
match self { match self {
Self::Value { value, .. } => value.downcast_ref(), Self::Value { value, .. } => value.downcast_ref(),
Self::Serialized { .. } => None, Self::Serialized(_) => None,
} }
} }
@@ -181,7 +188,7 @@ impl Element {
pub(crate) fn get_mut_temp<T: 'static>(&mut self) -> Option<&mut T> { pub(crate) fn get_mut_temp<T: 'static>(&mut self) -> Option<&mut T> {
match self { match self {
Self::Value { value, .. } => value.downcast_mut(), Self::Value { value, .. } => value.downcast_mut(),
Self::Serialized { .. } => None, Self::Serialized(_) => None,
} }
} }
@@ -196,14 +203,14 @@ impl Element {
*self = Self::new_temp(insert_with()); *self = Self::new_temp(insert_with());
} }
} }
Self::Serialized { .. } => { Self::Serialized(_) => {
*self = Self::new_temp(insert_with()); *self = Self::new_temp(insert_with());
} }
} }
match self { match self {
Self::Value { value, .. } => value.downcast_mut().unwrap(), // This unwrap will never panic because we already converted object to required type Self::Value { value, .. } => value.downcast_mut().unwrap(), // This unwrap will never panic because we already converted object to required type
Self::Serialized { .. } => unreachable!(), Self::Serialized(_) => unreachable!(),
} }
} }
@@ -220,19 +227,19 @@ impl Element {
} }
#[cfg(feature = "persistence")] #[cfg(feature = "persistence")]
Self::Serialized { ron, .. } => { Self::Serialized(SerializedElement { ron, .. }) => {
*self = Self::new_persisted(from_ron_str::<T>(ron).unwrap_or_else(insert_with)); *self = Self::new_persisted(from_ron_str::<T>(ron).unwrap_or_else(insert_with));
} }
#[cfg(not(feature = "persistence"))] #[cfg(not(feature = "persistence"))]
Self::Serialized { .. } => { Self::Serialized(_) => {
*self = Self::new_persisted(insert_with()); *self = Self::new_persisted(insert_with());
} }
} }
match self { match self {
Self::Value { value, .. } => value.downcast_mut().unwrap(), // This unwrap will never panic because we already converted object to required type Self::Value { value, .. } => value.downcast_mut().unwrap(), // This unwrap will never panic because we already converted object to required type
Self::Serialized { .. } => unreachable!(), Self::Serialized(_) => unreachable!(),
} }
} }
@@ -241,17 +248,17 @@ impl Element {
Self::Value { value, .. } => value.downcast_mut(), Self::Value { value, .. } => value.downcast_mut(),
#[cfg(feature = "persistence")] #[cfg(feature = "persistence")]
Self::Serialized { ron, .. } => { Self::Serialized(SerializedElement { ron, .. }) => {
*self = Self::new_persisted(from_ron_str::<T>(ron)?); *self = Self::new_persisted(from_ron_str::<T>(ron)?);
match self { match self {
Self::Value { value, .. } => value.downcast_mut(), Self::Value { value, .. } => value.downcast_mut(),
Self::Serialized { .. } => unreachable!(), Self::Serialized(_) => unreachable!(),
} }
} }
#[cfg(not(feature = "persistence"))] #[cfg(not(feature = "persistence"))]
Self::Serialized { .. } => None, Self::Serialized(_) => None,
} }
} }
@@ -268,15 +275,13 @@ impl Element {
Some(SerializedElement { Some(SerializedElement {
type_id: (**value).type_id().into(), type_id: (**value).type_id().into(),
ron: ron.into(), ron: ron.into(),
generation: 1,
}) })
} else { } else {
None None
} }
} }
Self::Serialized { type_id, ron } => Some(SerializedElement { Self::Serialized(element) => Some(element.clone()),
type_id: *type_id,
ron: ron.clone(),
}),
} }
} }
} }
@@ -341,23 +346,36 @@ use crate::Id;
/// assert_eq!(map.get_persisted::<f64>(b), Some(13.37)); /// assert_eq!(map.get_persisted::<f64>(b), Some(13.37));
/// assert_eq!(map.get_temp::<String>(b), Some("Hello World".to_owned())); /// assert_eq!(map.get_temp::<String>(b), Some("Hello World".to_owned()));
/// ``` /// ```
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug)]
// We store use `id XOR typeid` as a key, so we don't need to hash again! // We use `id XOR typeid` as a key, so we don't need to hash again!
pub struct IdTypeMap(nohash_hasher::IntMap<u64, Element>); pub struct IdTypeMap {
map: nohash_hasher::IntMap<u64, Element>,
max_bytes_per_type: usize,
}
impl Default for IdTypeMap {
fn default() -> Self {
Self {
map: Default::default(),
max_bytes_per_type: 256 * 1024,
}
}
}
impl IdTypeMap { impl IdTypeMap {
/// Insert a value that will not be persisted. /// Insert a value that will not be persisted.
#[inline] #[inline]
pub fn insert_temp<T: 'static + Any + Clone + Send + Sync>(&mut self, id: Id, value: T) { pub fn insert_temp<T: 'static + Any + Clone + Send + Sync>(&mut self, id: Id, value: T) {
let hash = hash(TypeId::of::<T>(), id); let hash = hash(TypeId::of::<T>(), id);
self.0.insert(hash, Element::new_temp(value)); self.map.insert(hash, Element::new_temp(value));
} }
/// Insert a value that will be persisted next time you start the app. /// Insert a value that will be persisted next time you start the app.
#[inline] #[inline]
pub fn insert_persisted<T: SerializableAny>(&mut self, id: Id, value: T) { pub fn insert_persisted<T: SerializableAny>(&mut self, id: Id, value: T) {
let hash = hash(TypeId::of::<T>(), id); let hash = hash(TypeId::of::<T>(), id);
self.0.insert(hash, Element::new_persisted(value)); self.map.insert(hash, Element::new_persisted(value));
} }
/// Read a value without trying to deserialize a persisted value. /// Read a value without trying to deserialize a persisted value.
@@ -366,7 +384,7 @@ impl IdTypeMap {
#[inline] #[inline]
pub fn get_temp<T: 'static + Clone>(&self, id: Id) -> Option<T> { pub fn get_temp<T: 'static + Clone>(&self, id: Id) -> Option<T> {
let hash = hash(TypeId::of::<T>(), id); let hash = hash(TypeId::of::<T>(), id);
self.0.get(&hash).and_then(|x| x.get_temp()).cloned() self.map.get(&hash).and_then(|x| x.get_temp()).cloned()
} }
/// Read a value, optionally deserializing it if available. /// Read a value, optionally deserializing it if available.
@@ -378,7 +396,7 @@ impl IdTypeMap {
#[inline] #[inline]
pub fn get_persisted<T: SerializableAny>(&mut self, id: Id) -> Option<T> { pub fn get_persisted<T: SerializableAny>(&mut self, id: Id) -> Option<T> {
let hash = hash(TypeId::of::<T>(), id); let hash = hash(TypeId::of::<T>(), id);
self.0 self.map
.get_mut(&hash) .get_mut(&hash)
.and_then(|x| x.get_mut_persisted()) .and_then(|x| x.get_mut_persisted())
.cloned() .cloned()
@@ -418,7 +436,7 @@ impl IdTypeMap {
) -> &mut T { ) -> &mut T {
let hash = hash(TypeId::of::<T>(), id); let hash = hash(TypeId::of::<T>(), id);
use std::collections::hash_map::Entry; use std::collections::hash_map::Entry;
match self.0.entry(hash) { match self.map.entry(hash) {
Entry::Vacant(vacant) => vacant Entry::Vacant(vacant) => vacant
.insert(Element::new_temp(insert_with())) .insert(Element::new_temp(insert_with()))
.get_mut_temp() .get_mut_temp()
@@ -436,7 +454,7 @@ impl IdTypeMap {
) -> &mut T { ) -> &mut T {
let hash = hash(TypeId::of::<T>(), id); let hash = hash(TypeId::of::<T>(), id);
use std::collections::hash_map::Entry; use std::collections::hash_map::Entry;
match self.0.entry(hash) { match self.map.entry(hash) {
Entry::Vacant(vacant) => vacant Entry::Vacant(vacant) => vacant
.insert(Element::new_persisted(insert_with())) .insert(Element::new_persisted(insert_with()))
.get_mut_persisted() .get_mut_persisted()
@@ -447,17 +465,28 @@ impl IdTypeMap {
} }
} }
/// For tests
#[cfg(feature = "persistence")]
#[allow(unused)]
fn get_generation<T: SerializableAny>(&self, id: Id) -> Option<usize> {
let element = self.map.get(&hash(TypeId::of::<T>(), id))?;
match element {
Element::Value { .. } => Some(0),
Element::Serialized(SerializedElement { generation, .. }) => Some(*generation),
}
}
/// Remove the state of this type an id. /// Remove the state of this type an id.
#[inline] #[inline]
pub fn remove<T: 'static>(&mut self, id: Id) { pub fn remove<T: 'static>(&mut self, id: Id) {
let hash = hash(TypeId::of::<T>(), id); let hash = hash(TypeId::of::<T>(), id);
self.0.remove(&hash); self.map.remove(&hash);
} }
/// Note all state of the given type. /// Note all state of the given type.
pub fn remove_by_type<T: 'static>(&mut self) { pub fn remove_by_type<T: 'static>(&mut self) {
let key = TypeId::of::<T>(); let key = TypeId::of::<T>();
self.0.retain(|_, e| { self.map.retain(|_, e| {
let e: &Element = e; let e: &Element = e;
e.type_id() != key e.type_id() != key
}); });
@@ -465,32 +494,32 @@ impl IdTypeMap {
#[inline] #[inline]
pub fn clear(&mut self) { pub fn clear(&mut self) {
self.0.clear(); self.map.clear();
} }
#[inline] #[inline]
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.0.is_empty() self.map.is_empty()
} }
#[inline] #[inline]
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.0.len() self.map.len()
} }
/// Count how many values are stored but not yet deserialized. /// Count how many values are stored but not yet deserialized.
#[inline] #[inline]
pub fn count_serialized(&self) -> usize { pub fn count_serialized(&self) -> usize {
self.0 self.map
.values() .values()
.filter(|e| matches!(e, Element::Serialized { .. })) .filter(|e| matches!(e, Element::Serialized(_)))
.count() .count()
} }
/// Count the number of values are stored with the given type. /// Count the number of values are stored with the given type.
pub fn count<T: 'static>(&self) -> usize { pub fn count<T: 'static>(&self) -> usize {
let key = TypeId::of::<T>(); let key = TypeId::of::<T>();
self.0 self.map
.iter() .iter()
.filter(|(_, e)| { .filter(|(_, e)| {
let e: &Element = e; let e: &Element = e;
@@ -498,6 +527,28 @@ impl IdTypeMap {
}) })
.count() .count()
} }
/// The maximum number of bytes that will be used to
/// store the persisted state of a single widget type.
///
/// Some egui widgets store persisted state that is
/// serialized to disk by some backends (e.g. `eframe`).
///
/// Example of such widgets is `CollapsingHeader` and `Window`.
/// If you keep creating widgets with unique ids (e.g. `Windows` with many different names),
/// egui will use up more and more space for these widgets, until this limit is reached.
///
/// Once this limit is reached, the state that was read the longest time ago will be dropped first.
///
/// This value in itself will not be serialized.
pub fn max_bytes_per_type(&self) -> usize {
self.max_bytes_per_type
}
/// See [`Self::max_bytes_per_type`].
pub fn set_max_bytes_per_type(&mut self, max_bytes_per_type: usize) {
self.max_bytes_per_type = max_bytes_per_type;
}
} }
#[inline(always)] #[inline(always)]
@@ -516,25 +567,94 @@ struct PersistedMap(Vec<(u64, SerializedElement)>);
impl PersistedMap { impl PersistedMap {
fn from_map(map: &IdTypeMap) -> Self { fn from_map(map: &IdTypeMap) -> Self {
crate::profile_function!(); crate::profile_function!();
// filter out the elements which cannot be serialized:
Self( use std::collections::BTreeMap;
map.0
.iter() let mut types_map: nohash_hasher::IntMap<TypeId, TypeStats> = Default::default();
.filter_map(|(&hash, element)| Some((hash, element.to_serialize()?))) #[derive(Default)]
.collect(), struct TypeStats {
) num_bytes: usize,
generations: BTreeMap<usize, GenerationStats>,
}
#[derive(Default)]
struct GenerationStats {
num_bytes: usize,
elements: Vec<(u64, SerializedElement)>,
}
let max_bytes_per_type = map.max_bytes_per_type;
{
crate::profile_scope!("gather");
for (hash, element) in &map.map {
if let Some(element) = element.to_serialize() {
let mut stats = types_map.entry(element.type_id).or_default();
stats.num_bytes += element.ron.len();
let mut generation_stats =
stats.generations.entry(element.generation).or_default();
generation_stats.num_bytes += element.ron.len();
generation_stats.elements.push((*hash, element));
} else {
// temporary value that shouldn't be serialized
}
}
}
let mut persisted = vec![];
{
crate::profile_scope!("gc");
for stats in types_map.values() {
let mut bytes_written = 0;
// Start with the most recently read values, and then go as far as we are allowed.
// Always include at least one generation.
for generation in stats.generations.values() {
if bytes_written == 0
|| bytes_written + generation.num_bytes <= max_bytes_per_type
{
persisted.append(&mut generation.elements.clone());
bytes_written += generation.num_bytes;
} else {
// Omit the rest. The user hasn't read the values in a while.
break;
}
}
}
}
Self(persisted)
} }
fn into_map(self) -> IdTypeMap { fn into_map(self) -> IdTypeMap {
crate::profile_function!(); crate::profile_function!();
IdTypeMap( let map = self
self.0 .0
.into_iter() .into_iter()
.map(|(hash, SerializedElement { type_id, ron })| { .map(
(hash, Element::Serialized { type_id, ron }) |(
}) hash,
.collect(), SerializedElement {
type_id,
ron,
generation,
},
)| {
(
hash,
Element::Serialized(SerializedElement {
type_id,
ron,
generation: generation + 1, // This is where we increment the generation!
}),
) )
},
)
.collect();
IdTypeMap {
map,
..Default::default()
}
} }
} }
@@ -731,3 +851,129 @@ fn test_mix_serialize() {
); );
assert_eq!(map.get_temp::<Serializable>(id), Some(Serializable(555))); assert_eq!(map.get_temp::<Serializable>(id), Some(Serializable(555)));
} }
#[cfg(feature = "persistence")]
#[test]
fn test_serialize_generations() {
use serde::{Deserialize, Serialize};
fn serialize_and_deserialize(map: &IdTypeMap) -> IdTypeMap {
let serialized = ron::to_string(map).unwrap();
ron::from_str(&serialized).unwrap()
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
struct A(i32);
let mut map: IdTypeMap = Default::default();
for i in 0..3 {
map.insert_persisted(Id::new(i), A(i));
}
for i in 0..3 {
assert_eq!(map.get_generation::<A>(Id::new(i)), Some(0));
}
map = serialize_and_deserialize(&map);
// We use generation 0 for non-serilized,
// 1 for things that have been serialized but never deserialized,
// and then we increment with 1 on each deserialize.
// So we should have generation 2 now:
for i in 0..3 {
assert_eq!(map.get_generation::<A>(Id::new(i)), Some(2));
}
// Reading should reset:
assert_eq!(map.get_persisted::<A>(Id::new(0)), Some(A(0)));
assert_eq!(map.get_generation::<A>(Id::new(0)), Some(0));
// Generations should increment:
map = serialize_and_deserialize(&map);
assert_eq!(map.get_generation::<A>(Id::new(0)), Some(2));
assert_eq!(map.get_generation::<A>(Id::new(1)), Some(3));
}
#[cfg(feature = "persistence")]
#[test]
fn test_serialize_gc() {
use serde::{Deserialize, Serialize};
fn serialize_and_deserialize(mut map: IdTypeMap, max_bytes_per_type: usize) -> IdTypeMap {
map.set_max_bytes_per_type(max_bytes_per_type);
let serialized = ron::to_string(&map).unwrap();
ron::from_str(&serialized).unwrap()
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
struct A(usize);
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
struct B(usize);
let mut map: IdTypeMap = Default::default();
let num_a = 1_000;
let num_b = 10;
for i in 0..num_a {
map.insert_persisted(Id::new(i), A(i));
}
for i in 0..num_b {
map.insert_persisted(Id::new(i), B(i));
}
map = serialize_and_deserialize(map, 100);
// We always serialize at least one generation:
assert_eq!(map.count::<A>(), num_a);
assert_eq!(map.count::<B>(), num_b);
// Create a new small generation:
map.insert_persisted(Id::new(1_000_000), A(1_000_000));
map.insert_persisted(Id::new(1_000_000), B(1_000_000));
assert_eq!(map.count::<A>(), num_a + 1);
assert_eq!(map.count::<B>(), num_b + 1);
// And read a value:
assert_eq!(map.get_persisted::<A>(Id::new(0)), Some(A(0)));
assert_eq!(map.get_persisted::<B>(Id::new(0)), Some(B(0)));
map = serialize_and_deserialize(map, 100);
assert_eq!(
map.count::<A>(),
2,
"We should have dropped the oldest generation, but kept the new value and the read value"
);
assert_eq!(
map.count::<B>(),
num_b + 1,
"B should fit under the byte limit"
);
// Create another small generation:
map.insert_persisted(Id::new(2_000_000), A(2_000_000));
map.insert_persisted(Id::new(2_000_000), B(2_000_000));
map = serialize_and_deserialize(map, 100);
assert_eq!(map.count::<A>(), 3); // The read value, plus the two new ones
assert_eq!(map.count::<B>(), num_b + 2); // all the old ones, plus two new ones
// Lower the limit, and we should only have the latest generation:
map = serialize_and_deserialize(map, 1);
assert_eq!(map.count::<A>(), 1);
assert_eq!(map.count::<B>(), 1);
assert_eq!(
map.get_persisted::<A>(Id::new(2_000_000)),
Some(A(2_000_000))
);
assert_eq!(
map.get_persisted::<B>(Id::new(2_000_000)),
Some(B(2_000_000))
);
}

View File

@@ -1,5 +1,4 @@
use std::borrow::Cow; use std::{borrow::Cow, sync::Arc};
use std::sync::Arc;
use crate::{ use crate::{
style::WidgetVisuals, text::LayoutJob, Align, Color32, FontFamily, FontSelection, Galley, Pos2, style::WidgetVisuals, text::LayoutJob, Align, Color32, FontFamily, FontSelection, Galley, Pos2,
@@ -25,6 +24,8 @@ use crate::{
pub struct RichText { pub struct RichText {
text: String, text: String,
size: Option<f32>, size: Option<f32>,
extra_letter_spacing: f32,
line_height: Option<f32>,
family: Option<FontFamily>, family: Option<FontFamily>,
text_style: Option<TextStyle>, text_style: Option<TextStyle>,
background_color: Color32, background_color: Color32,
@@ -100,6 +101,32 @@ impl RichText {
self self
} }
/// Extra spacing between letters, in points.
///
/// Default: 0.0.
///
/// For even text it is recommended you round this to an even number of _pixels_,
/// e.g. using [`crate::Painter::round_to_pixel`].
#[inline]
pub fn extra_letter_spacing(mut self, extra_letter_spacing: f32) -> Self {
self.extra_letter_spacing = extra_letter_spacing;
self
}
/// Explicit line height of the text in points.
///
/// This is the distance between the bottom row of two subsequent lines of text.
///
/// If `None` (the default), the line height is determined by the font.
///
/// For even text it is recommended you round this to an even number of _pixels_,
/// e.g. using [`crate::Painter::round_to_pixel`].
#[inline]
pub fn line_height(mut self, line_height: Option<f32>) -> Self {
self.line_height = line_height;
self
}
/// Select the font family. /// Select the font family.
/// ///
/// This overrides the value from [`Self::text_style`]. /// This overrides the value from [`Self::text_style`].
@@ -253,6 +280,8 @@ impl RichText {
let Self { let Self {
text, text,
size, size,
extra_letter_spacing,
line_height,
family, family,
text_style, text_style,
background_color, background_color,
@@ -309,6 +338,8 @@ impl RichText {
let text_format = crate::text::TextFormat { let text_format = crate::text::TextFormat {
font_id, font_id,
extra_letter_spacing,
line_height,
color: text_color, color: text_color,
background: background_color, background: background_color,
italics, italics,

View File

@@ -155,7 +155,7 @@ impl BackendPanel {
// On web, the browser controls `pixels_per_point`. // On web, the browser controls `pixels_per_point`.
let integration_controls_pixels_per_point = frame.is_web(); let integration_controls_pixels_per_point = frame.is_web();
if !integration_controls_pixels_per_point { if !integration_controls_pixels_per_point {
self.pixels_per_point_ui(ui, &frame.info()); self.pixels_per_point_ui(ui, frame.info());
} }
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]

View File

@@ -35,7 +35,8 @@ impl Default for Demos {
Box::<super::sliders::Sliders>::default(), Box::<super::sliders::Sliders>::default(),
Box::<super::strip_demo::StripDemo>::default(), Box::<super::strip_demo::StripDemo>::default(),
Box::<super::table_demo::TableDemo>::default(), Box::<super::table_demo::TableDemo>::default(),
Box::<super::text_edit::TextEdit>::default(), Box::<super::text_edit::TextEditDemo>::default(),
Box::<super::text_layout::TextLayoutDemo>::default(),
Box::<super::widget_gallery::WidgetGallery>::default(), Box::<super::widget_gallery::WidgetGallery>::default(),
Box::<super::window_options::WindowOptions>::default(), Box::<super::window_options::WindowOptions>::default(),
Box::<super::tests::WindowResizeTest>::default(), Box::<super::tests::WindowResizeTest>::default(),

View File

@@ -1,5 +1,5 @@
use super::*; use super::*;
use egui::{epaint::text::TextWrapping, *}; use egui::*;
/// Showcase some ui code /// Showcase some ui code
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
@@ -7,8 +7,6 @@ use egui::{epaint::text::TextWrapping, *};
pub struct MiscDemoWindow { pub struct MiscDemoWindow {
num_columns: usize, num_columns: usize,
text_break: TextBreakDemo,
widgets: Widgets, widgets: Widgets,
colors: ColorWidgets, colors: ColorWidgets,
custom_collapsing_header: CustomCollapsingHeader, custom_collapsing_header: CustomCollapsingHeader,
@@ -24,8 +22,6 @@ impl Default for MiscDemoWindow {
MiscDemoWindow { MiscDemoWindow {
num_columns: 2, num_columns: 2,
text_break: Default::default(),
widgets: Default::default(), widgets: Default::default(),
colors: Default::default(), colors: Default::default(),
custom_collapsing_header: Default::default(), custom_collapsing_header: Default::default(),
@@ -72,8 +68,6 @@ impl View for MiscDemoWindow {
.default_open(false) .default_open(false)
.show(ui, |ui| { .show(ui, |ui| {
text_layout_demo(ui); text_layout_demo(ui);
ui.separator();
self.text_break.ui(ui);
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
ui.add(crate::egui_github_link_file_line!()); ui.add(crate::egui_github_link_file_line!());
}); });
@@ -644,63 +638,3 @@ fn text_layout_demo(ui: &mut Ui) {
ui.label(job); ui.label(job);
} }
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
struct TextBreakDemo {
break_anywhere: bool,
max_rows: usize,
overflow_character: Option<char>,
}
impl Default for TextBreakDemo {
fn default() -> Self {
Self {
max_rows: 1,
break_anywhere: true,
overflow_character: Some('…'),
}
}
}
impl TextBreakDemo {
pub fn ui(&mut self, ui: &mut Ui) {
let Self {
break_anywhere,
max_rows,
overflow_character,
} = self;
use egui::text::LayoutJob;
ui.horizontal(|ui| {
ui.add(DragValue::new(max_rows));
ui.label("Max rows");
});
ui.horizontal(|ui| {
ui.label("Line-break:");
ui.radio_value(break_anywhere, false, "word boundaries");
ui.radio_value(break_anywhere, true, "anywhere");
});
ui.horizontal(|ui| {
ui.selectable_value(overflow_character, None, "None");
ui.selectable_value(overflow_character, Some('…'), "");
ui.selectable_value(overflow_character, Some('—'), "");
ui.selectable_value(overflow_character, Some('-'), " - ");
ui.label("Overflow character");
});
let mut job =
LayoutJob::single_section(crate::LOREM_IPSUM_LONG.to_owned(), TextFormat::default());
job.wrap = TextWrapping {
max_rows: *max_rows,
break_anywhere: *break_anywhere,
overflow_character: *overflow_character,
..Default::default()
};
ui.label(job); // `Label` overrides some of the wrapping settings, e.g. wrap width
}
}

View File

@@ -26,6 +26,7 @@ pub mod strip_demo;
pub mod table_demo; pub mod table_demo;
pub mod tests; pub mod tests;
pub mod text_edit; pub mod text_edit;
pub mod text_layout;
pub mod toggle_switch; pub mod toggle_switch;
pub mod widget_gallery; pub mod widget_gallery;
pub mod window_options; pub mod window_options;

View File

@@ -1,12 +1,12 @@
/// Showcase [`TextEdit`]. /// Showcase [`egui::TextEdit`].
#[derive(PartialEq, Eq)] #[derive(PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))] #[cfg_attr(feature = "serde", serde(default))]
pub struct TextEdit { pub struct TextEditDemo {
pub text: String, pub text: String,
} }
impl Default for TextEdit { impl Default for TextEditDemo {
fn default() -> Self { fn default() -> Self {
Self { Self {
text: "Edit this text".to_owned(), text: "Edit this text".to_owned(),
@@ -14,7 +14,7 @@ impl Default for TextEdit {
} }
} }
impl super::Demo for TextEdit { impl super::Demo for TextEditDemo {
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"🖹 TextEdit" "🖹 TextEdit"
} }
@@ -30,7 +30,7 @@ impl super::Demo for TextEdit {
} }
} }
impl super::View for TextEdit { impl super::View for TextEditDemo {
fn ui(&mut self, ui: &mut egui::Ui) { fn ui(&mut self, ui: &mut egui::Ui) {
let Self { text } = self; let Self { text } = self;

View File

@@ -0,0 +1,135 @@
/// Showcase text layout
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct TextLayoutDemo {
break_anywhere: bool,
max_rows: usize,
overflow_character: Option<char>,
extra_letter_spacing_pixels: i32,
line_height_pixels: u32,
}
impl Default for TextLayoutDemo {
fn default() -> Self {
Self {
max_rows: 3,
break_anywhere: true,
overflow_character: Some('…'),
extra_letter_spacing_pixels: 0,
line_height_pixels: 0,
}
}
}
impl super::Demo for TextLayoutDemo {
fn name(&self) -> &'static str {
"🖹 Text Layout"
}
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
egui::Window::new(self.name())
.open(open)
.resizable(true)
.show(ctx, |ui| {
use super::View as _;
self.ui(ui);
});
}
}
impl super::View for TextLayoutDemo {
fn ui(&mut self, ui: &mut egui::Ui) {
let Self {
break_anywhere,
max_rows,
overflow_character,
extra_letter_spacing_pixels,
line_height_pixels,
} = self;
use egui::text::LayoutJob;
let pixels_per_point = ui.ctx().pixels_per_point();
let points_per_pixel = 1.0 / pixels_per_point;
ui.vertical_centered(|ui| {
ui.add(crate::egui_github_link_file_line!());
});
ui.add_space(12.0);
egui::Grid::new("TextLayoutDemo")
.num_columns(2)
.show(ui, |ui| {
ui.label("Max rows:");
ui.add(egui::DragValue::new(max_rows));
ui.end_row();
ui.label("Line-break:");
ui.horizontal(|ui| {
ui.radio_value(break_anywhere, false, "word boundaries");
ui.radio_value(break_anywhere, true, "anywhere");
});
ui.end_row();
ui.label("Overflow character:");
ui.horizontal(|ui| {
ui.selectable_value(overflow_character, None, "None");
ui.selectable_value(overflow_character, Some('…'), "");
ui.selectable_value(overflow_character, Some('—'), "");
ui.selectable_value(overflow_character, Some('-'), " - ");
});
ui.end_row();
ui.label("Extra letter spacing:");
ui.add(egui::DragValue::new(extra_letter_spacing_pixels).suffix(" pixels"));
ui.end_row();
ui.label("Line height:");
ui.horizontal(|ui| {
if ui
.selectable_label(*line_height_pixels == 0, "Default")
.clicked()
{
*line_height_pixels = 0;
}
if ui
.selectable_label(*line_height_pixels != 0, "Custom")
.clicked()
{
*line_height_pixels = (pixels_per_point * 20.0).round() as _;
}
if *line_height_pixels != 0 {
ui.add(egui::DragValue::new(line_height_pixels).suffix(" pixels"));
}
});
ui.end_row();
});
ui.add_space(12.0);
egui::ScrollArea::vertical().show(ui, |ui| {
let extra_letter_spacing = points_per_pixel * *extra_letter_spacing_pixels as f32;
let line_height =
(*line_height_pixels != 0).then_some(points_per_pixel * *line_height_pixels as f32);
let mut job = LayoutJob::single_section(
crate::LOREM_IPSUM_LONG.to_owned(),
egui::TextFormat {
extra_letter_spacing,
line_height,
..Default::default()
},
);
job.wrap = egui::text::TextWrapping {
max_rows: *max_rows,
break_anywhere: *break_anywhere,
overflow_character: *overflow_character,
..Default::default()
};
// NOTE: `Label` overrides some of the wrapping settings, e.g. wrap width
ui.label(job);
});
}
}

View File

@@ -188,5 +188,6 @@ fn format_from_style(
underline, underline,
strikethrough, strikethrough,
valign, valign,
..Default::default()
} }
} }

View File

@@ -5,6 +5,17 @@
//! Create some [`Shape`]:s and pass them to [`tessellate_shapes`] to generate [`Mesh`]:es //! Create some [`Shape`]:s and pass them to [`tessellate_shapes`] to generate [`Mesh`]:es
//! that you can then paint using some graphics API of your choice (e.g. OpenGL). //! that you can then paint using some graphics API of your choice (e.g. OpenGL).
//! //!
//! ## Coordinate system
//! The left-top corner of the screen is `(0.0, 0.0)`,
//! with X increasing to the right and Y increasing downwards.
//!
//! `epaint` uses logical _points_ as its coordinate system.
//! Those related to physical _pixels_ by the `pixels_per_point` scale factor.
//! For example, a high-dpi screeen can have `pixels_per_point = 2.0`,
//! meaning there are two physical screen pixels for each logical point.
//!
//! Angles are in radians, and are measured clockwise from the X-axis, which has angle=0.
//!
//! ## Feature flags //! ## Feature flags
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())] #![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
//! //!

View File

@@ -49,11 +49,6 @@ pub struct GlyphInfo {
/// Unit: points. /// Unit: points.
pub ascent: f32, pub ascent: f32,
/// row height computed from the font metrics.
///
/// Unit: points.
pub row_height: f32,
/// Texture coordinates. /// Texture coordinates.
pub uv_rect: UvRect, pub uv_rect: UvRect,
} }
@@ -65,7 +60,6 @@ impl Default for GlyphInfo {
id: ab_glyph::GlyphId(0), id: ab_glyph::GlyphId(0),
advance_width: 0.0, advance_width: 0.0,
ascent: 0.0, ascent: 0.0,
row_height: 0.0,
uv_rect: Default::default(), uv_rect: Default::default(),
} }
} }
@@ -250,7 +244,7 @@ impl FontImpl {
/ self.pixels_per_point / self.pixels_per_point
} }
/// Height of one row of text. In points /// Height of one row of text in points.
#[inline(always)] #[inline(always)]
pub fn row_height(&self) -> f32 { pub fn row_height(&self) -> f32 {
self.height_in_points self.height_in_points
@@ -312,7 +306,6 @@ impl FontImpl {
id: glyph_id, id: glyph_id,
advance_width: advance_width_in_points, advance_width: advance_width_in_points,
ascent: self.ascent, ascent: self.ascent,
row_height: self.row_height(),
uv_rect, uv_rect,
} }
} }

View File

@@ -653,7 +653,7 @@ impl FontsImpl {
self.font(font_id).has_glyphs(s) self.font(font_id).has_glyphs(s)
} }
/// Height of one row of text. In points /// Height of one row of text in points.
fn row_height(&mut self, font_id: &FontId) -> f32 { fn row_height(&mut self, font_id: &FontId) -> f32 {
self.font(font_id).row_height() self.font(font_id).row_height()
} }

View File

@@ -113,11 +113,15 @@ fn layout_section(
format, format,
} = section; } = section;
let font = fonts.font(&format.font_id); let font = fonts.font(&format.font_id);
let font_height = font.row_height(); let line_height = section
.format
.line_height
.unwrap_or_else(|| font.row_height());
let extra_letter_spacing = section.format.extra_letter_spacing;
let mut paragraph = out_paragraphs.last_mut().unwrap(); let mut paragraph = out_paragraphs.last_mut().unwrap();
if paragraph.glyphs.is_empty() { if paragraph.glyphs.is_empty() {
paragraph.empty_paragraph_height = font_height; // TODO(emilk): replace this hack with actually including `\n` in the glyphs? paragraph.empty_paragraph_height = line_height; // TODO(emilk): replace this hack with actually including `\n` in the glyphs?
} }
paragraph.cursor_x += leading_space; paragraph.cursor_x += leading_space;
@@ -128,19 +132,20 @@ fn layout_section(
if job.break_on_newline && chr == '\n' { if job.break_on_newline && chr == '\n' {
out_paragraphs.push(Paragraph::default()); out_paragraphs.push(Paragraph::default());
paragraph = out_paragraphs.last_mut().unwrap(); paragraph = out_paragraphs.last_mut().unwrap();
paragraph.empty_paragraph_height = font_height; // TODO(emilk): replace this hack with actually including `\n` in the glyphs? paragraph.empty_paragraph_height = line_height; // TODO(emilk): replace this hack with actually including `\n` in the glyphs?
} else { } else {
let (font_impl, glyph_info) = font.glyph_info_and_font_impl(chr); let (font_impl, glyph_info) = font.glyph_info_and_font_impl(chr);
if let Some(font_impl) = font_impl { if let Some(font_impl) = font_impl {
if let Some(last_glyph_id) = last_glyph_id { if let Some(last_glyph_id) = last_glyph_id {
paragraph.cursor_x += font_impl.pair_kerning(last_glyph_id, glyph_info.id); paragraph.cursor_x += font_impl.pair_kerning(last_glyph_id, glyph_info.id);
paragraph.cursor_x += extra_letter_spacing;
} }
} }
paragraph.glyphs.push(Glyph { paragraph.glyphs.push(Glyph {
chr, chr,
pos: pos2(paragraph.cursor_x, f32::NAN), pos: pos2(paragraph.cursor_x, f32::NAN),
size: vec2(glyph_info.advance_width, glyph_info.row_height), size: vec2(glyph_info.advance_width, line_height),
ascent: glyph_info.ascent, ascent: glyph_info.ascent,
uv_rect: glyph_info.uv_rect, uv_rect: glyph_info.uv_rect,
section_index, section_index,
@@ -328,8 +333,12 @@ fn replace_last_glyph_with_overflow_character(
}; };
let section = &job.sections[last_glyph.section_index as usize]; let section = &job.sections[last_glyph.section_index as usize];
let extra_letter_spacing = section.format.extra_letter_spacing;
let font = fonts.font(&section.format.font_id); let font = fonts.font(&section.format.font_id);
let font_height = font.row_height(); let line_height = section
.format
.line_height
.unwrap_or_else(|| font.row_height());
let prev_glyph_id = prev_glyph.map(|prev_glyph| { let prev_glyph_id = prev_glyph.map(|prev_glyph| {
let (_, prev_glyph_info) = font.glyph_info_and_font_impl(prev_glyph.chr); let (_, prev_glyph_info) = font.glyph_info_and_font_impl(prev_glyph.chr);
@@ -338,22 +347,28 @@ fn replace_last_glyph_with_overflow_character(
// undo kerning with previous glyph // undo kerning with previous glyph
let (font_impl, glyph_info) = font.glyph_info_and_font_impl(last_glyph.chr); let (font_impl, glyph_info) = font.glyph_info_and_font_impl(last_glyph.chr);
last_glyph.pos.x -= font_impl last_glyph.pos.x -= extra_letter_spacing
+ font_impl
.zip(prev_glyph_id) .zip(prev_glyph_id)
.map(|(font_impl, prev_glyph_id)| font_impl.pair_kerning(prev_glyph_id, glyph_info.id)) .map(|(font_impl, prev_glyph_id)| {
font_impl.pair_kerning(prev_glyph_id, glyph_info.id)
})
.unwrap_or_default(); .unwrap_or_default();
// replace the glyph // replace the glyph
last_glyph.chr = overflow_character; last_glyph.chr = overflow_character;
let (font_impl, glyph_info) = font.glyph_info_and_font_impl(last_glyph.chr); let (font_impl, glyph_info) = font.glyph_info_and_font_impl(last_glyph.chr);
last_glyph.size = vec2(glyph_info.advance_width, font_height); last_glyph.size = vec2(glyph_info.advance_width, line_height);
last_glyph.uv_rect = glyph_info.uv_rect; last_glyph.uv_rect = glyph_info.uv_rect;
last_glyph.ascent = glyph_info.ascent; last_glyph.ascent = glyph_info.ascent;
// reapply kerning // reapply kerning
last_glyph.pos.x += font_impl last_glyph.pos.x += extra_letter_spacing
+ font_impl
.zip(prev_glyph_id) .zip(prev_glyph_id)
.map(|(font_impl, prev_glyph_id)| font_impl.pair_kerning(prev_glyph_id, glyph_info.id)) .map(|(font_impl, prev_glyph_id)| {
font_impl.pair_kerning(prev_glyph_id, glyph_info.id)
})
.unwrap_or_default(); .unwrap_or_default();
row.rect.max.x = last_glyph.max_x(); row.rect.max.x = last_glyph.max_x();
@@ -474,7 +489,7 @@ fn galley_from_rows(
let mut min_x: f32 = 0.0; let mut min_x: f32 = 0.0;
let mut max_x: f32 = 0.0; let mut max_x: f32 = 0.0;
for row in &mut rows { for row in &mut rows {
let mut row_height = first_row_min_height.max(row.rect.height()); let mut line_height = first_row_min_height.max(row.rect.height());
let mut row_ascent = 0.0f32; let mut row_ascent = 0.0f32;
first_row_min_height = 0.0; first_row_min_height = 0.0;
@@ -484,10 +499,10 @@ fn galley_from_rows(
.iter() .iter()
.max_by(|a, b| a.size.y.partial_cmp(&b.size.y).unwrap()) .max_by(|a, b| a.size.y.partial_cmp(&b.size.y).unwrap())
{ {
row_height = glyph.size.y; line_height = glyph.size.y;
row_ascent = glyph.ascent; row_ascent = glyph.ascent;
} }
row_height = point_scale.round_to_pixel(row_height); line_height = point_scale.round_to_pixel(line_height);
// Now positions each glyph: // Now positions each glyph:
for glyph in &mut row.glyphs { for glyph in &mut row.glyphs {
@@ -503,11 +518,11 @@ fn galley_from_rows(
} }
row.rect.min.y = cursor_y; row.rect.min.y = cursor_y;
row.rect.max.y = cursor_y + row_height; row.rect.max.y = cursor_y + line_height;
min_x = min_x.min(row.rect.min.x); min_x = min_x.min(row.rect.min.x);
max_x = max_x.max(row.rect.max.x); max_x = max_x.max(row.rect.max.x);
cursor_y += row_height; cursor_y += line_height;
cursor_y = point_scale.round_to_pixel(cursor_y); cursor_y = point_scale.round_to_pixel(cursor_y);
} }

View File

@@ -221,11 +221,28 @@ impl std::hash::Hash for LayoutSection {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
#[derive(Clone, Debug, Hash, PartialEq)] /// Formatting option for a section of text.
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct TextFormat { pub struct TextFormat {
pub font_id: FontId, pub font_id: FontId,
/// Extra spacing between letters, in points.
///
/// Default: 0.0.
///
/// For even text it is recommended you round this to an even number of _pixels_.
pub extra_letter_spacing: f32,
/// Explicit line height of the text in points.
///
/// This is the distance between the bottom row of two subsequent lines of text.
///
/// If `None` (the default), the line height is determined by the font.
///
/// For even text it is recommended you round this to an even number of _pixels_.
pub line_height: Option<f32>,
/// Text color /// Text color
pub color: Color32, pub color: Color32,
@@ -248,6 +265,8 @@ impl Default for TextFormat {
fn default() -> Self { fn default() -> Self {
Self { Self {
font_id: FontId::default(), font_id: FontId::default(),
extra_letter_spacing: 0.0,
line_height: None,
color: Color32::GRAY, color: Color32::GRAY,
background: Color32::TRANSPARENT, background: Color32::TRANSPARENT,
italics: false, italics: false,
@@ -258,6 +277,34 @@ impl Default for TextFormat {
} }
} }
impl std::hash::Hash for TextFormat {
#[inline]
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
let Self {
font_id,
extra_letter_spacing,
line_height,
color,
background,
italics,
underline,
strikethrough,
valign,
} = self;
font_id.hash(state);
crate::f32_hash(state, *extra_letter_spacing);
if let Some(line_height) = *line_height {
crate::f32_hash(state, line_height);
}
color.hash(state);
background.hash(state);
italics.hash(state);
underline.hash(state);
strikethrough.hash(state);
valign.hash(state);
}
}
impl TextFormat { impl TextFormat {
#[inline] #[inline]
pub fn simple(font_id: FontId, color: Color32) -> Self { pub fn simple(font_id: FontId, color: Color32) -> Self {
@@ -486,10 +533,12 @@ pub struct Glyph {
/// `ascent` value from the font /// `ascent` value from the font
pub ascent: f32, pub ascent: f32,
/// Advance width and font row height. /// Advance width and line height.
///
/// Does not control the visual size of the glyph (see [`Self::uv_rect`] for that).
pub size: Vec2, pub size: Vec2,
/// Position of the glyph in the font texture, in texels. /// Position and size of the glyph in the font texture, in texels.
pub uv_rect: UvRect, pub uv_rect: UvRect,
/// Index into [`LayoutJob::sections`]. Decides color etc. /// Index into [`LayoutJob::sections`]. Decides color etc.